@twinfinity/twin 6.0.1 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/BuildInfo.js +3 -3
  2. package/dist/TwinClient.d.ts +227 -0
  3. package/dist/TwinClient.d.ts.map +1 -1
  4. package/dist/TwinClient.js +474 -28
  5. package/dist/TwinClient.js.map +1 -1
  6. package/dist/errors.d.ts +122 -3
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +156 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/tsconfig.tsbuildinfo +1 -1
  11. package/dist/types/asset-attachment-api.d.ts +51 -0
  12. package/dist/types/asset-attachment-api.d.ts.map +1 -0
  13. package/dist/types/asset-attachment-api.js +8 -0
  14. package/dist/types/asset-attachment-api.js.map +1 -0
  15. package/dist/types/asset-types-api.d.ts +76 -0
  16. package/dist/types/asset-types-api.d.ts.map +1 -0
  17. package/dist/types/asset-types-api.js +5 -0
  18. package/dist/types/asset-types-api.js.map +1 -0
  19. package/dist/types/index.d.ts +3 -0
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/index.js +3 -0
  22. package/dist/types/index.js.map +1 -1
  23. package/dist/types/twin-permissions-api.d.ts +20 -0
  24. package/dist/types/twin-permissions-api.d.ts.map +1 -0
  25. package/dist/types/twin-permissions-api.js +2 -0
  26. package/dist/types/twin-permissions-api.js.map +1 -0
  27. package/docs/assets/hierarchy.js +1 -1
  28. package/docs/assets/navigation.js +1 -1
  29. package/docs/assets/search.js +1 -1
  30. package/docs/classes/AssetAttachmentConflictError.html +3 -0
  31. package/docs/classes/AssetAttachmentNotFoundError.html +2 -0
  32. package/docs/classes/AssetBatchError.html +1 -1
  33. package/docs/classes/AssetCatalogTooLargeError.html +16 -0
  34. package/docs/classes/AssetCategoryConflictError.html +3 -0
  35. package/docs/classes/AssetCategoryForbiddenError.html +2 -0
  36. package/docs/classes/AssetCategoryNotFoundError.html +2 -0
  37. package/docs/classes/AssetCategoryValidationError.html +3 -0
  38. package/docs/classes/AssetConflictError.html +1 -1
  39. package/docs/classes/AssetForbiddenError.html +1 -1
  40. package/docs/classes/AssetNotFoundError.html +1 -1
  41. package/docs/classes/AssetTypeConflictError.html +3 -0
  42. package/docs/classes/AssetTypeForbiddenError.html +2 -0
  43. package/docs/classes/AssetTypeNotFoundError.html +2 -0
  44. package/docs/classes/AssetTypeValidationError.html +3 -0
  45. package/docs/classes/AssetValidationError.html +1 -1
  46. package/docs/classes/ProcessorNotFoundError.html +1 -1
  47. package/docs/classes/TwinClient.html +154 -48
  48. package/docs/classes/TwinConflictError.html +1 -1
  49. package/docs/classes/TwinFileConflictError.html +1 -1
  50. package/docs/classes/TwinFileNotFoundError.html +1 -1
  51. package/docs/classes/TwinFileValidationError.html +1 -1
  52. package/docs/classes/TwinNotFoundError.html +1 -1
  53. package/docs/classes/TwinUnauthenticatedError.html +6 -0
  54. package/docs/classes/TwinValidationError.html +1 -1
  55. package/docs/functions/isAssetCatalogTooLargeErrorBody.html +6 -0
  56. package/docs/functions/readErrorBody.html +10 -0
  57. package/docs/hierarchy.html +1 -1
  58. package/docs/interfaces/ApiCreateUploadSessionResponse.html +7 -0
  59. package/docs/interfaces/AssetAttachmentPut.html +6 -0
  60. package/docs/interfaces/AssetCatalogCategory.html +7 -0
  61. package/docs/interfaces/AssetCatalogResponse.html +6 -0
  62. package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +15 -0
  63. package/docs/interfaces/AssetCategoryDelete.html +2 -0
  64. package/docs/interfaces/AssetCategoryGet.html +14 -0
  65. package/docs/interfaces/AssetCategoryPut.html +7 -0
  66. package/docs/interfaces/AssetTypeDelete.html +2 -0
  67. package/docs/interfaces/AssetTypeGet.html +16 -0
  68. package/docs/interfaces/AssetTypePut.html +9 -0
  69. package/docs/interfaces/AttachmentGet.html +20 -0
  70. package/docs/interfaces/AttachmentPost.html +4 -0
  71. package/docs/interfaces/ITwinClient.html +155 -49
  72. package/docs/interfaces/MyPermissions.html +7 -0
  73. package/docs/interfaces/TwinErrorBody.html +2 -2
  74. package/docs/interfaces/TwinSpecification.html +1 -1
  75. package/docs/interfaces/UploadStatusGet.html +3 -0
  76. package/docs/modules.html +1 -1
  77. package/docs/types/TwinCapability.html +6 -0
  78. package/docs/types/TwinErrorCause.html +8 -0
  79. package/docs/types/TwinErrorItemKind.html +4 -0
  80. package/docs/types/UploadStatus.html +1 -0
  81. package/package.json +3 -3
  82. package/src/BuildInfo.ts +41 -41
  83. package/src/TwinClient.ts +2334 -1496
  84. package/src/errors.ts +379 -117
  85. package/src/index.ts +3 -3
  86. package/src/types/Twin.ts +7 -7
  87. package/src/types/TwinGeometryInfo.ts +7 -7
  88. package/src/types/asset-api.ts +60 -60
  89. package/src/types/asset-attachment-api.ts +56 -0
  90. package/src/types/asset-batch-api.ts +88 -88
  91. package/src/types/asset-types-api.ts +86 -0
  92. package/src/types/common.ts +10 -10
  93. package/src/types/generated.ts +574 -574
  94. package/src/types/index.ts +9 -6
  95. package/src/types/twin-api.ts +99 -99
  96. package/src/types/twin-file-api.ts +67 -67
  97. package/src/types/twin-permissions-api.ts +20 -0
package/src/TwinClient.ts CHANGED
@@ -1,1496 +1,2334 @@
1
- import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
2
- import {
3
- readT8ClientModelWithFactories,
4
- readT8ClientModelWithGeometry,
5
- readT8GIModelWithFactories,
6
- T8ClientEntityTypeDTO,
7
- T8ClientModel,
8
- T8ClientModelDTO,
9
- T8ClientProductConnectionsDTO,
10
- T8ClientProductDTO,
11
- T8ClientProjectConnectionsDTO,
12
- T8ClientProjectDTO,
13
- T8ClientUnitDTO,
14
- T8EntityType,
15
- T8GIMeshDescriptor,
16
- T8GIMeshDescriptorDTO,
17
- T8GIStyle,
18
- T8GIStyleDTO,
19
- T8GIRegion,
20
- T8GIRegionDTO,
21
- T8Product,
22
- T8ProductWithGeometry,
23
- T8Project,
24
- T8Unit
25
- } from '@twinfinity/t8-client-model';
26
- import type { T8ProjectWithGeometry } from '@twinfinity/t8-client-model';
27
- import type { T8GIModelGeneric } from '@twinfinity/t8-client-model';
28
- import type {
29
- PaginatedResponse,
30
- ListTwinItem,
31
- TwinGet,
32
- TwinPut,
33
- TwinVersionContentResponse,
34
- TwinProcessorEvent,
35
- TwinDiagnosticsResponse,
36
- AssetListItem,
37
- AssetGet,
38
- AssetPost,
39
- AssetPut,
40
- AssetBatchOperation,
41
- AssetBatchResponse,
42
- AssetBatchGetResponse,
43
- TwinFileGet,
44
- TwinFilePut,
45
- TwinFileDelete,
46
- TwinFileListOptions,
47
- TwinFileVersionListOptions,
48
- TwinContentResponse
49
- } from './types';
50
- import {
51
- TwinValidationError,
52
- TwinConflictError,
53
- TwinNotFoundError,
54
- ProcessorNotFoundError,
55
- AssetValidationError,
56
- AssetForbiddenError,
57
- AssetConflictError,
58
- AssetNotFoundError,
59
- AssetBatchError,
60
- TwinFileValidationError,
61
- TwinFileConflictError,
62
- TwinFileNotFoundError
63
- } from './errors';
64
- import { Twin } from './types/Twin';
65
- import { TwinGeometryInfo } from './types/TwinGeometryInfo';
66
-
67
- export interface ITwinClient {
68
- /**
69
- * @param relatedTo - Filter twins by container ID.
70
- * @throws {TwinValidationError} On invalid query parameters (400).
71
- */
72
- listTwins(
73
- relatedTo?: string,
74
- page?: string,
75
- limit?: number,
76
- signal?: AbortSignal
77
- ): Promise<PaginatedResponse<ListTwinItem>>;
78
-
79
- /**
80
- * Fetches all twins by auto-paginating through {@link listTwins}. Caps at 1000 pages.
81
- * @param relatedTo - Filter twins by container ID.
82
- */
83
- listAllTwins(relatedTo?: string, signal?: AbortSignal): Promise<ListTwinItem[]>;
84
-
85
- /**
86
- * @param twin - Twin ID string, or `{id, version}` to fetch a specific version.
87
- * @throws {TwinNotFoundError} When the twin or version does not exist (404).
88
- */
89
- getTwin(twin: string | { id: string; version: number }, signal?: AbortSignal): Promise<TwinGet>;
90
-
91
- /**
92
- * @throws {TwinValidationError} On invalid query parameters (400).
93
- */
94
- listTwinVersions(
95
- id: string,
96
- options?: {
97
- includeDeleted?: boolean;
98
- page?: string;
99
- limit?: number;
100
- },
101
- signal?: AbortSignal
102
- ): Promise<PaginatedResponse<ListTwinItem>>;
103
-
104
- downloadTwinContent(
105
- twinId: string,
106
- twinVersion: number,
107
- content: TwinContentResponse,
108
- signal?: AbortSignal
109
- ): Promise<ArrayBuffer>;
110
-
111
- /**
112
- * Downloads and deserializes the T8 client model (t8cm + t8cm-ext) using default types.
113
- * When no version is specified, searches versions newest-first for one containing t8cm content.
114
- *
115
- * @param twin - Twin ID string, or `{id, version}` to load a specific version.
116
- * @throws {Error} When no version has t8cm content.
117
- */
118
- loadTwin(
119
- twin: string | { id: string; version: number },
120
- signal?: AbortSignal
121
- ): Promise<Twin<T8Project, T8Product<T8EntityType>, T8EntityType, T8Unit>>;
122
-
123
- /**
124
- * Downloads and deserializes the T8 client model using custom factory functions for domain-specific types.
125
- * When no version is specified, searches versions newest-first for one containing t8cm content.
126
- *
127
- * @param twin - Twin ID string, or `{id, version}` to load a specific version.
128
- * @throws {Error} When no version has t8cm content.
129
- */
130
- loadTwinWithFactories<TProject, TProduct, TEntityType, TUnit>(
131
- twin: string | { id: string; version: number },
132
- projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
133
- productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
134
- entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
135
- unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
136
- connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
137
- connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
138
- signal?: AbortSignal
139
- ): Promise<Twin<TProject, TProduct, TEntityType, TUnit>>;
140
-
141
- /**
142
- * Downloads and deserializes geometry info (t8gi) using default types.
143
- * When no version is specified, searches versions newest-first for one containing t8gi content.
144
- *
145
- * @param twin - Twin ID string, or `{id, version}` to load a specific version.
146
- * @throws {Error} When no version has t8gi content.
147
- */
148
- loadGeometryInfo(
149
- twin: string | { id: string; version: number },
150
- signal?: AbortSignal
151
- ): Promise<TwinGeometryInfo<T8GIMeshDescriptor, T8GIStyle, T8GIRegion>>;
152
-
153
- /**
154
- * Downloads and deserializes geometry info (t8gi) using custom factory functions.
155
- * When no version is specified, searches versions newest-first for one containing t8gi content.
156
- *
157
- * @param twin - Twin ID string, or `{id, version}` to load a specific version.
158
- * @throws {Error} When no version has t8gi content.
159
- */
160
- loadGeometryInfoWithFactories<TMeshDescriptor, TStyle, TRegion>(
161
- twin: string | { id: string; version: number },
162
- meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
163
- styleFactory: (dto: T8GIStyleDTO) => TStyle,
164
- regionFactory: (dto: T8GIRegionDTO) => TRegion,
165
- signal?: AbortSignal
166
- ): Promise<TwinGeometryInfo<TMeshDescriptor, TStyle, TRegion>>;
167
-
168
- /**
169
- * Downloads and deserializes both the client model (t8cm) and geometry (t8gi/t8gd) into a combined result.
170
- * When no version is specified, searches versions newest-first for one containing both t8cm and t8gi content.
171
- *
172
- * @param twin - Twin ID string, or `{id, version}` to load a specific version.
173
- * @throws {Error} When no version has both t8cm and t8gi content.
174
- */
175
- loadTwinWithGeometry(
176
- twin: string | { id: string; version: number },
177
- signal?: AbortSignal
178
- ): Promise<Twin<T8ProjectWithGeometry, T8ProductWithGeometry, T8EntityType, T8Unit>>;
179
-
180
- /**
181
- * Creates a new twin or updates an existing one (upsert). Provide `etag` in the twin body to update.
182
- *
183
- * @throws {TwinValidationError} On invalid request body (400).
184
- * @throws {TwinConflictError} On etag mismatch (409). The error's `currentTwin` contains the server's current state.
185
- */
186
- createOrUpdateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet>;
187
-
188
- /** Alias for {@link createOrUpdateTwin}. */
189
- updateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet>;
190
-
191
- /**
192
- * @throws {TwinNotFoundError} When the twin does not exist (404).
193
- * @throws {TwinConflictError} On etag mismatch (409). The error's `currentTwin` contains the server's current state.
194
- */
195
- restoreTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet>;
196
-
197
- /**
198
- * @throws {TwinNotFoundError} When the twin or version does not exist (404).
199
- */
200
- getTwinVersionContent(
201
- twinId: string,
202
- twinVersion: number,
203
- signal?: AbortSignal
204
- ): Promise<TwinVersionContentResponse>;
205
-
206
- /**
207
- * Returns content for the latest twin version.
208
- * @throws {TwinNotFoundError} When the twin does not exist (404).
209
- */
210
- getTwinContent(twinId: string, signal?: AbortSignal): Promise<TwinVersionContentResponse>;
211
-
212
- /** @throws {TwinNotFoundError} When the twin does not exist (404). */
213
- getTwinDiagnostics(twinId: string, signal?: AbortSignal): Promise<TwinDiagnosticsResponse>;
214
-
215
- /** @throws {TwinNotFoundError} When the twin or version does not exist (404). */
216
- getTwinVersionDiagnostics(
217
- twinId: string,
218
- twinVersion: number,
219
- signal?: AbortSignal
220
- ): Promise<TwinDiagnosticsResponse>;
221
-
222
- /**
223
- * @throws {TwinValidationError} On invalid query parameters (400).
224
- * @throws {TwinNotFoundError} When the twin or version does not exist (404).
225
- */
226
- getTwinVersionEvents(
227
- twinId: string,
228
- twinVersion: number,
229
- offset?: number,
230
- limit?: number,
231
- signal?: AbortSignal
232
- ): Promise<PaginatedResponse<TwinProcessorEvent>>;
233
-
234
- /**
235
- * @throws {TwinValidationError} On invalid query parameters (400).
236
- * @throws {ProcessorNotFoundError} When the processor does not exist for this twin version (404).
237
- */
238
- getProcessorEvents(
239
- twinId: string,
240
- twinVersion: number,
241
- processorId: string,
242
- offset?: number,
243
- limit?: number,
244
- signal?: AbortSignal
245
- ): Promise<PaginatedResponse<TwinProcessorEvent>>;
246
-
247
- /**
248
- * @throws {AssetValidationError} On invalid query parameters (400).
249
- * @throws {AssetNotFoundError} When the twin does not exist (404).
250
- */
251
- listAssets(
252
- twinId: string,
253
- offset?: number,
254
- limit?: number,
255
- options?: { includeDeleted?: boolean },
256
- signal?: AbortSignal
257
- ): Promise<PaginatedResponse<AssetListItem>>;
258
-
259
- /** @throws {AssetNotFoundError} When the asset does not exist (404). */
260
- getAsset(twinId: string, assetId: string, signal?: AbortSignal): Promise<AssetGet>;
261
-
262
- /** @throws {AssetValidationError} On invalid request body (400). The error's `errorBody` has structured detail. */
263
- createAssets(twinId: string, assets: AssetPost[], signal?: AbortSignal): Promise<AssetGet[]>;
264
-
265
- /**
266
- * @throws {AssetValidationError} On invalid request body (400). The error's `errorBody` has structured detail.
267
- * @throws {AssetConflictError} On etag mismatch (409). The error's `currentAsset` contains the server's current state.
268
- */
269
- updateAsset(twinId: string, assetId: string, asset: AssetPut, signal?: AbortSignal): Promise<AssetGet>;
270
-
271
- /**
272
- * @throws {AssetNotFoundError} When the asset does not exist (404).
273
- * @throws {AssetConflictError} On etag mismatch (409). The error's `currentAsset` contains the server's current state.
274
- */
275
- deleteAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet>;
276
-
277
- /**
278
- * @throws {AssetNotFoundError} When the asset does not exist (404).
279
- * @throws {AssetConflictError} On etag mismatch (409). The error's `currentAsset` contains the server's current state.
280
- */
281
- restoreAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet>;
282
-
283
- /**
284
- * Executes a batch of asset operations (upsert, delete, restore) atomically.
285
- * All operations succeed or all are rolled back.
286
- *
287
- * @param operations - Array of batch operations (max 1000).
288
- * @throws {AssetValidationError} On validation failure (400).
289
- * @throws {AssetForbiddenError} On insufficient permissions (403).
290
- * @throws {AssetBatchError} On conflict (409) or not found (404). The error's `response` has per-item results with reverted statuses.
291
- */
292
- batchAssets(twinId: string, operations: AssetBatchOperation[], signal?: AbortSignal): Promise<AssetBatchResponse>;
293
-
294
- /**
295
- * Retrieves multiple assets by ID in a single request.
296
- *
297
- * @param ids - Array of asset IDs (max 1000).
298
- * @throws {AssetValidationError} On validation failure (400).
299
- */
300
- batchGetAssets(twinId: string, ids: string[], signal?: AbortSignal): Promise<AssetBatchGetResponse>;
301
-
302
- /**
303
- * @throws {TwinNotFoundError} When the twin does not exist (404).
304
- * @throws {TwinConflictError} On etag mismatch (409). The error's `currentTwin` contains the server's current state.
305
- */
306
- deleteTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet>;
307
-
308
- /** @throws {TwinFileValidationError} On invalid query parameters (400). */
309
- listTwinFiles(
310
- twinId: string,
311
- options?: TwinFileListOptions,
312
- signal?: AbortSignal
313
- ): Promise<PaginatedResponse<TwinFileGet>>;
314
-
315
- /**
316
- * @throws {TwinFileNotFoundError} When the file does not exist (404).
317
- * @throws {TwinFileValidationError} On invalid request (400).
318
- */
319
- getTwinFile(twinId: string, fileId: string, signal?: AbortSignal): Promise<TwinFileGet>;
320
-
321
- /**
322
- * @throws {TwinFileValidationError} On invalid request body (400).
323
- * @throws {TwinFileConflictError} On etag mismatch (409). The error's `currentTwinFile` contains the server's current state.
324
- */
325
- createOrUpdateTwinFile(
326
- twinId: string,
327
- fileId: string,
328
- twinFile: TwinFilePut,
329
- signal?: AbortSignal
330
- ): Promise<TwinFileGet>;
331
-
332
- /**
333
- * @throws {TwinFileNotFoundError} When the file does not exist (404).
334
- * @throws {TwinFileConflictError} On etag mismatch (409). The error's `currentTwinFile` contains the server's current state.
335
- * @throws {TwinFileValidationError} On invalid request (400).
336
- */
337
- deleteTwinFile(
338
- twinId: string,
339
- fileId: string,
340
- deleteRequest: TwinFileDelete,
341
- signal?: AbortSignal
342
- ): Promise<TwinFileGet>;
343
-
344
- /**
345
- * @throws {TwinFileNotFoundError} When the file does not exist (404).
346
- * @throws {TwinFileConflictError} On etag mismatch (409). The error's `currentTwinFile` contains the server's current state.
347
- * @throws {TwinFileValidationError} On invalid request (400).
348
- */
349
- restoreTwinFile(twinId: string, fileId: string, etag: string, signal?: AbortSignal): Promise<TwinFileGet>;
350
-
351
- /** @throws {TwinFileNotFoundError} When the file does not exist (404). */
352
- getTwinFileContent(twinId: string, fileId: string, signal?: AbortSignal): Promise<Blob>;
353
-
354
- /** @throws {TwinFileNotFoundError} When the file or version does not exist (404). */
355
- getTwinFileVersion(twinId: string, fileId: string, version: number, signal?: AbortSignal): Promise<TwinFileGet>;
356
-
357
- /** @throws {TwinFileValidationError} On invalid query parameters (400). */
358
- listTwinFileVersions(
359
- twinId: string,
360
- fileId: string,
361
- options?: TwinFileVersionListOptions,
362
- signal?: AbortSignal
363
- ): Promise<PaginatedResponse<TwinFileGet>>;
364
-
365
- /** @throws {TwinFileNotFoundError} When the file or version does not exist (404). */
366
- getTwinFileVersionContent(twinId: string, fileId: string, version: number, signal?: AbortSignal): Promise<Blob>;
367
- }
368
-
369
- export class TwinClient implements ITwinClient {
370
- private readonly _baseUrl: string;
371
-
372
- constructor(
373
- baseUrl: string | URL,
374
- private readonly _httpClient: TwinfinityHttpClient
375
- ) {
376
- this._baseUrl = `${baseUrl}/_ps/twins`;
377
- }
378
-
379
- async listTwins(
380
- relatedTo?: string,
381
- page: string | undefined = undefined,
382
- limit = 100,
383
- signal?: AbortSignal
384
- ): Promise<PaginatedResponse<ListTwinItem>> {
385
- const params = new URLSearchParams();
386
- if (relatedTo) params.set('relatedTo', relatedTo);
387
- if (page !== undefined) params.set('page', page);
388
- if (limit !== 100) params.set('limit', limit.toString());
389
-
390
- const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
391
-
392
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
393
- headers: { 'Content-Type': 'application/json' },
394
- signal
395
- });
396
-
397
- if (response.ok) {
398
- return (await response.json()) as PaginatedResponse<ListTwinItem>;
399
- }
400
-
401
- if (response.status === 400) {
402
- const errorBody = await response.json();
403
- throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
404
- }
405
-
406
- const errorText = await response.text();
407
- throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
408
- }
409
-
410
- async listAllTwins(relatedTo?: string, signal?: AbortSignal): Promise<ListTwinItem[]> {
411
- const allTwins: ListTwinItem[] = [];
412
- let page: string | undefined = undefined;
413
- const limit = 100;
414
- const maxPages = 1000;
415
-
416
- for (let pageCount = 0; pageCount < maxPages; pageCount++) {
417
- const response = await this.listTwins(relatedTo, page, limit, signal);
418
- allTwins.push(...response.data);
419
-
420
- if (response.data.length < limit) {
421
- break;
422
- }
423
-
424
- page = response.pagination.nextPage;
425
- }
426
-
427
- return allTwins;
428
- }
429
-
430
- async getTwin(twin: string | { id: string; version: number }, signal?: AbortSignal): Promise<TwinGet> {
431
- const twinId = typeof twin === 'string' ? twin : twin.id;
432
- const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
433
- const response = await this._httpClient.fetch(
434
- HttpMethod.Get,
435
- `${this._baseUrl}/${twinId}${specifiedVersion !== undefined ? `/versions/${specifiedVersion}` : ''}`,
436
- {
437
- headers: { 'Content-Type': 'application/json' },
438
- signal
439
- }
440
- );
441
-
442
- if (response.ok) {
443
- return (await response.json()) as TwinGet;
444
- }
445
-
446
- if (response.status === 404) {
447
- throw new TwinNotFoundError(`Twin ${twinId} not found`);
448
- }
449
-
450
- const errorText = await response.text();
451
- throw new Error(`Failed to get twin: ${response.status} ${response.statusText} - ${errorText}`);
452
- }
453
-
454
- async listTwinVersions(
455
- id: string,
456
- options?: {
457
- includeDeleted?: boolean;
458
- page: string | undefined;
459
- limit?: number;
460
- },
461
- signal?: AbortSignal
462
- ): Promise<PaginatedResponse<ListTwinItem>> {
463
- const params = new URLSearchParams();
464
- if (options?.page !== undefined) params.set('page', options.page);
465
- if (options?.limit !== undefined && options.limit !== 100) params.set('limit', options.limit.toString());
466
- if (options?.includeDeleted) params.set('includeDeleted', 'true');
467
-
468
- const url = `${this._baseUrl}/${id}/versions${params.toString() ? `?${params}` : ''}`;
469
-
470
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
471
- headers: { 'Content-Type': 'application/json' },
472
- signal
473
- });
474
-
475
- if (response.ok) {
476
- return (await response.json()) as PaginatedResponse<ListTwinItem>;
477
- }
478
-
479
- if (response.status === 400) {
480
- const errorBody = await response.json();
481
- throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
482
- }
483
-
484
- const errorText = await response.text();
485
- throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
486
- }
487
-
488
- async downloadTwinContent(
489
- twinId: string,
490
- twinVersion: number,
491
- content: TwinContentResponse,
492
- signal?: AbortSignal
493
- ): Promise<ArrayBuffer> {
494
- const response = await this._httpClient.fetch(
495
- HttpMethod.Get,
496
- `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents/${content.id}/content`,
497
- { signal }
498
- );
499
- if (response.ok) {
500
- const blob = await response.blob();
501
- return await blob.arrayBuffer();
502
- }
503
-
504
- throw new Error(`Failed to download twin content: ${response.status} ${response.statusText}`);
505
- }
506
-
507
- private async loadTwinVersion<TProject, TProduct, TEntityType, TUnit>(
508
- twinId: string,
509
- twinVersion: number,
510
- projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
511
- productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
512
- entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
513
- unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
514
- connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
515
- connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
516
- signal?: AbortSignal
517
- ): Promise<T8ClientModel<TProject, TProduct, TEntityType, TUnit> | undefined> {
518
- const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
519
-
520
- const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
521
-
522
- if (t8cmItem === undefined) {
523
- return undefined;
524
- }
525
-
526
- const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
527
-
528
- const data = await Promise.all(
529
- [this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal)].concat(
530
- t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal))
531
- )
532
- );
533
-
534
- return readT8ClientModelWithFactories<TProject, TProduct, TEntityType, TUnit>(
535
- data,
536
- projectFactory,
537
- productFactory,
538
- entityTypeFactory,
539
- unitFactory,
540
- connectProject,
541
- connectProduct
542
- );
543
- }
544
-
545
- loadTwin(
546
- twin: string | { id: string; version: number },
547
- signal?: AbortSignal
548
- ): Promise<Twin<T8Project, T8Product<T8EntityType>, T8EntityType, T8Unit>> {
549
- return this.loadTwinWithFactories(
550
- twin,
551
- T8Project.create<T8Product<T8EntityType>, T8Unit>,
552
- T8Product.create<T8EntityType, T8Unit>,
553
- T8EntityType.create,
554
- T8Unit.create,
555
- T8Project.connect,
556
- T8Product.connect,
557
- signal
558
- );
559
- }
560
-
561
- async loadTwinWithFactories<TProject, TProduct, TEntityType, TUnit>(
562
- twin: string | { id: string; version: number },
563
- projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
564
- productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
565
- entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
566
- unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
567
- connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
568
- connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
569
- signal?: AbortSignal
570
- ): Promise<Twin<TProject, TProduct, TEntityType, TUnit>> {
571
- let page: string | undefined = undefined;
572
- const twinId = typeof twin === 'string' ? twin : twin.id;
573
- const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
574
- if (specifiedVersion !== undefined) {
575
- return {
576
- id: twinId,
577
- version: specifiedVersion,
578
- model: await this.loadTwinVersion(
579
- twinId,
580
- specifiedVersion,
581
- projectFactory,
582
- productFactory,
583
- entityTypeFactory,
584
- unitFactory,
585
- connectProject,
586
- connectProduct,
587
- signal
588
- )
589
- };
590
- }
591
-
592
- const maxPages = 100;
593
- for (let pageCount = 0; pageCount < maxPages; pageCount++) {
594
- const versions = await this.listTwinVersions(twinId, { page }, signal);
595
- if (versions.data.length === 0) {
596
- break;
597
- }
598
- for (const versionItem of versions.data) {
599
- const loadedModel = await this.loadTwinVersion(
600
- twinId,
601
- versionItem.version,
602
- projectFactory,
603
- productFactory,
604
- entityTypeFactory,
605
- unitFactory,
606
- connectProject,
607
- connectProduct,
608
- signal
609
- );
610
- if (loadedModel !== undefined) {
611
- return {
612
- id: twinId,
613
- version: versionItem.version,
614
- model: loadedModel
615
- };
616
- }
617
- }
618
- page = versions.pagination.nextPage;
619
- }
620
-
621
- throw new Error(`Twin ${twinId} does not have a client model`);
622
- }
623
-
624
- private async loadGeometryInfoVersion<TMeshDescriptor, TStyle, TRegion>(
625
- twinId: string,
626
- twinVersion: number,
627
- meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
628
- styleFactory: (dto: T8GIStyleDTO) => TStyle,
629
- regionFactory: (dto: T8GIRegionDTO) => TRegion,
630
- signal?: AbortSignal
631
- ): Promise<T8GIModelGeneric<TMeshDescriptor, TStyle, TRegion> | undefined> {
632
- const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
633
-
634
- const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
635
-
636
- if (t8giItem === undefined) {
637
- return undefined;
638
- }
639
-
640
- const data = await this.downloadTwinContent(twinId, twinVersion, t8giItem, signal);
641
-
642
- return readT8GIModelWithFactories(data, meshDescriptorFactory, styleFactory, regionFactory);
643
- }
644
-
645
- loadGeometryInfo(
646
- twin: string | { id: string; version: number },
647
- signal?: AbortSignal
648
- ): Promise<TwinGeometryInfo<T8GIMeshDescriptor, T8GIStyle, T8GIRegion>> {
649
- return this.loadGeometryInfoWithFactories(
650
- twin,
651
- T8GIMeshDescriptor.create,
652
- T8GIStyle.create,
653
- T8GIRegion.create,
654
- signal
655
- );
656
- }
657
-
658
- async loadGeometryInfoWithFactories<TMeshDescriptor, TStyle, TRegion>(
659
- twin: string | { id: string; version: number },
660
- meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
661
- styleFactory: (dto: T8GIStyleDTO) => TStyle,
662
- regionFactory: (dto: T8GIRegionDTO) => TRegion,
663
- signal?: AbortSignal
664
- ): Promise<TwinGeometryInfo<TMeshDescriptor, TStyle, TRegion>> {
665
- let page: string | undefined = undefined;
666
- const twinId = typeof twin === 'string' ? twin : twin.id;
667
- const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
668
-
669
- if (specifiedVersion !== undefined) {
670
- return {
671
- id: twinId,
672
- version: specifiedVersion,
673
- model: await this.loadGeometryInfoVersion(
674
- twinId,
675
- specifiedVersion,
676
- meshDescriptorFactory,
677
- styleFactory,
678
- regionFactory,
679
- signal
680
- )
681
- };
682
- }
683
-
684
- const maxPages = 100;
685
- for (let pageCount = 0; pageCount < maxPages; pageCount++) {
686
- const versions = await this.listTwinVersions(twinId, { page }, signal);
687
- if (versions.data.length === 0) {
688
- break;
689
- }
690
- for (const versionItem of versions.data) {
691
- const model = await this.loadGeometryInfoVersion(
692
- twinId,
693
- versionItem.version,
694
- meshDescriptorFactory,
695
- styleFactory,
696
- regionFactory,
697
- signal
698
- );
699
- if (model !== undefined) {
700
- return {
701
- id: twinId,
702
- version: versionItem.version,
703
- model
704
- };
705
- }
706
- }
707
- page = versions.pagination.nextPage;
708
- }
709
-
710
- throw new Error(`Twin ${twinId} does not have geometry info`);
711
- }
712
-
713
- private async loadTwinWithGeometryVersion(
714
- twinId: string,
715
- twinVersion: number,
716
- signal?: AbortSignal
717
- ): Promise<T8ClientModel<T8ProjectWithGeometry, T8ProductWithGeometry, T8EntityType, T8Unit> | undefined> {
718
- const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
719
-
720
- const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
721
- const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
722
-
723
- if (t8cmItem === undefined || t8giItem === undefined) {
724
- return undefined;
725
- }
726
-
727
- const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
728
- const t8gdItem = twinContent.content.find((item) => item.format === 't8gd');
729
-
730
- const downloads = [
731
- this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal),
732
- ...t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal)),
733
- this.downloadTwinContent(twinId, twinVersion, t8giItem, signal),
734
- ...(t8gdItem ? [this.downloadTwinContent(twinId, twinVersion, t8gdItem, signal)] : [])
735
- ];
736
-
737
- const allData = await Promise.all(downloads);
738
-
739
- const t8cmCount = 1 + t8cmExtensions.length;
740
- const t8cmData = allData.slice(0, t8cmCount);
741
- const t8giData = allData[t8cmCount];
742
- const t8gdData = t8gdItem ? allData[t8cmCount + 1] : undefined;
743
-
744
- return readT8ClientModelWithGeometry(t8cmData, t8giData, t8gdData);
745
- }
746
-
747
- async loadTwinWithGeometry(
748
- twin: string | { id: string; version: number },
749
- signal?: AbortSignal
750
- ): Promise<Twin<T8ProjectWithGeometry, T8ProductWithGeometry, T8EntityType, T8Unit>> {
751
- let page: string | undefined = undefined;
752
- const twinId = typeof twin === 'string' ? twin : twin.id;
753
- const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
754
-
755
- if (specifiedVersion !== undefined) {
756
- return {
757
- id: twinId,
758
- version: specifiedVersion,
759
- model: await this.loadTwinWithGeometryVersion(twinId, specifiedVersion, signal)
760
- };
761
- }
762
-
763
- const maxPages = 100;
764
- for (let pageCount = 0; pageCount < maxPages; pageCount++) {
765
- const versions = await this.listTwinVersions(twinId, { page }, signal);
766
- if (versions.data.length === 0) {
767
- break;
768
- }
769
- for (const versionItem of versions.data) {
770
- const model = await this.loadTwinWithGeometryVersion(twinId, versionItem.version, signal);
771
- if (model !== undefined) {
772
- return {
773
- id: twinId,
774
- version: versionItem.version,
775
- model
776
- };
777
- }
778
- }
779
- page = versions.pagination.nextPage;
780
- }
781
-
782
- throw new Error(`Twin ${twinId} does not have a client model with geometry`);
783
- }
784
-
785
- async createOrUpdateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet> {
786
- const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
787
- headers: { 'Content-Type': 'application/json' },
788
- body: JSON.stringify(twin),
789
- signal
790
- });
791
-
792
- if (response.ok) {
793
- return (await response.json()) as TwinGet;
794
- }
795
-
796
- if (response.status === 400) {
797
- const errorBody = await response.json();
798
- throw new TwinValidationError(`Twin validation failed: ${errorBody.detail}`, errorBody);
799
- }
800
-
801
- if (response.status === 409) {
802
- const conflictedTwin = (await response.json()) as TwinGet;
803
- throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
804
- }
805
-
806
- const errorText = await response.text();
807
- throw new Error(`Failed to create/update twin: ${response.status} ${response.statusText} - ${errorText}`);
808
- }
809
-
810
- async updateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet> {
811
- return this.createOrUpdateTwin(twinId, twin, signal);
812
- }
813
-
814
- async deleteTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet> {
815
- const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}`, {
816
- headers: { 'Content-Type': 'application/json' },
817
- body: JSON.stringify({ etag }),
818
- signal
819
- });
820
-
821
- if (response.ok) {
822
- return (await response.json()) as TwinGet;
823
- }
824
-
825
- if (response.status === 404) {
826
- throw new TwinNotFoundError(`Twin ${twinId} not found`);
827
- }
828
-
829
- if (response.status === 409) {
830
- const conflictedTwin = (await response.json()) as TwinGet;
831
- throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
832
- }
833
-
834
- const errorText = await response.text();
835
- throw new Error(`Failed to delete twin: ${response.status} ${response.statusText} - ${errorText}`);
836
- }
837
-
838
- async restoreTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet> {
839
- const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/restore`, {
840
- headers: { 'Content-Type': 'application/json' },
841
- body: JSON.stringify({ etag }),
842
- signal
843
- });
844
-
845
- if (response.ok) {
846
- return (await response.json()) as TwinGet;
847
- }
848
-
849
- if (response.status === 404) {
850
- throw new TwinNotFoundError(`Twin ${twinId} not found`);
851
- }
852
-
853
- if (response.status === 409) {
854
- const conflictedTwin = (await response.json()) as TwinGet;
855
- throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
856
- }
857
-
858
- const errorText = await response.text();
859
- throw new Error(`Failed to restore twin: ${response.status} ${response.statusText} - ${errorText}`);
860
- }
861
-
862
- async getTwinVersionContent(
863
- twinId: string,
864
- twinVersion: number,
865
- signal?: AbortSignal
866
- ): Promise<TwinVersionContentResponse> {
867
- const response = await this._httpClient.fetch(
868
- HttpMethod.Get,
869
- `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`,
870
- { headers: { 'Content-Type': 'application/json' }, signal }
871
- );
872
-
873
- if (response.ok) {
874
- return (await response.json()) as TwinVersionContentResponse;
875
- }
876
-
877
- if (response.status === 404) {
878
- throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
879
- }
880
-
881
- const errorText = await response.text();
882
- throw new Error(`Failed to get twin version content: ${response.status} ${response.statusText} - ${errorText}`);
883
- }
884
-
885
- async getTwinContent(twinId: string, signal?: AbortSignal): Promise<TwinVersionContentResponse> {
886
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/contents`, {
887
- headers: { 'Content-Type': 'application/json' },
888
- signal
889
- });
890
-
891
- if (response.ok) {
892
- return (await response.json()) as TwinVersionContentResponse;
893
- }
894
-
895
- if (response.status === 404) {
896
- throw new TwinNotFoundError(`Twin ${twinId} not found`);
897
- }
898
-
899
- const errorText = await response.text();
900
- throw new Error(`Failed to get twin content: ${response.status} ${response.statusText} - ${errorText}`);
901
- }
902
-
903
- async getTwinDiagnostics(twinId: string, signal?: AbortSignal): Promise<TwinDiagnosticsResponse> {
904
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/diagnostics`, {
905
- headers: { 'Content-Type': 'application/json' },
906
- signal
907
- });
908
-
909
- if (response.ok) {
910
- return (await response.json()) as TwinDiagnosticsResponse;
911
- }
912
-
913
- if (response.status === 404) {
914
- throw new TwinNotFoundError(`Twin ${twinId} not found`);
915
- }
916
-
917
- const errorText = await response.text();
918
- throw new Error(`Failed to get twin diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
919
- }
920
-
921
- async getTwinVersionDiagnostics(
922
- twinId: string,
923
- twinVersion: number,
924
- signal?: AbortSignal
925
- ): Promise<TwinDiagnosticsResponse> {
926
- const response = await this._httpClient.fetch(
927
- HttpMethod.Get,
928
- `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`,
929
- { headers: { 'Content-Type': 'application/json' }, signal }
930
- );
931
-
932
- if (response.ok) {
933
- return (await response.json()) as TwinDiagnosticsResponse;
934
- }
935
-
936
- if (response.status === 404) {
937
- throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
938
- }
939
-
940
- const errorText = await response.text();
941
- throw new Error(
942
- `Failed to get twin version diagnostics: ${response.status} ${response.statusText} - ${errorText}`
943
- );
944
- }
945
-
946
- async getTwinVersionEvents(
947
- twinId: string,
948
- twinVersion: number,
949
- offset = 0,
950
- limit = 100,
951
- signal?: AbortSignal
952
- ): Promise<PaginatedResponse<TwinProcessorEvent>> {
953
- const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/events?offset=${offset}&limit=${limit}`;
954
-
955
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
956
- headers: { 'Content-Type': 'application/json' },
957
- signal
958
- });
959
-
960
- if (response.ok) {
961
- return (await response.json()) as PaginatedResponse<TwinProcessorEvent>;
962
- }
963
-
964
- if (response.status === 400) {
965
- const errorBody = await response.json();
966
- throw new TwinValidationError(`Twin events validation failed: ${errorBody.detail}`, errorBody);
967
- }
968
-
969
- if (response.status === 404) {
970
- throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
971
- }
972
-
973
- const errorText = await response.text();
974
- throw new Error(`Failed to get twin version events: ${response.status} ${response.statusText} - ${errorText}`);
975
- }
976
-
977
- async getProcessorEvents(
978
- twinId: string,
979
- twinVersion: number,
980
- processorId: string,
981
- offset = 0,
982
- limit = 100,
983
- signal?: AbortSignal
984
- ): Promise<PaginatedResponse<TwinProcessorEvent>> {
985
- const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/processors/${processorId}/events?offset=${offset}&limit=${limit}`;
986
-
987
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
988
- headers: { 'Content-Type': 'application/json' },
989
- signal
990
- });
991
-
992
- if (response.ok) {
993
- return (await response.json()) as PaginatedResponse<TwinProcessorEvent>;
994
- }
995
-
996
- if (response.status === 400) {
997
- const errorBody = await response.json();
998
- throw new TwinValidationError(`Processor events validation failed: ${errorBody.detail}`, errorBody);
999
- }
1000
-
1001
- if (response.status === 404) {
1002
- throw new ProcessorNotFoundError(
1003
- `Processor ${processorId} not found for twin ${twinId} version ${twinVersion}`
1004
- );
1005
- }
1006
-
1007
- const errorText = await response.text();
1008
- throw new Error(`Failed to get processor events: ${response.status} ${response.statusText} - ${errorText}`);
1009
- }
1010
-
1011
- async listAssets(
1012
- twinId: string,
1013
- offset = 0,
1014
- limit = 100,
1015
- options?: { includeDeleted?: boolean },
1016
- signal?: AbortSignal
1017
- ): Promise<PaginatedResponse<AssetListItem>> {
1018
- const includeDeleted = options?.includeDeleted ?? false;
1019
- const params = `offset=${offset}&limit=${limit}${includeDeleted ? '&includeDeleted=true' : ''}`;
1020
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets?${params}`, {
1021
- headers: { 'Content-Type': 'application/json' },
1022
- signal
1023
- });
1024
-
1025
- if (response.ok) {
1026
- return (await response.json()) as PaginatedResponse<AssetListItem>;
1027
- }
1028
-
1029
- if (response.status === 400) {
1030
- const errorBody = await response.json();
1031
- throw new AssetValidationError(`Asset listing validation failed: ${errorBody.detail}`, errorBody);
1032
- }
1033
-
1034
- if (response.status === 404) {
1035
- throw new AssetNotFoundError(`Twin ${twinId} not found`);
1036
- }
1037
-
1038
- const errorText = await response.text();
1039
- throw new Error(`Failed to list assets: ${response.status} ${response.statusText} - ${errorText}`);
1040
- }
1041
-
1042
- async getAsset(twinId: string, assetId: string, signal?: AbortSignal): Promise<AssetGet> {
1043
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
1044
- headers: { 'Content-Type': 'application/json' },
1045
- signal
1046
- });
1047
-
1048
- if (response.ok) {
1049
- return (await response.json()) as AssetGet;
1050
- }
1051
-
1052
- if (response.status === 404) {
1053
- throw new AssetNotFoundError(`Asset ${assetId} not found`);
1054
- }
1055
-
1056
- const errorText = await response.text();
1057
- throw new Error(`Failed to get asset: ${response.status} ${response.statusText} - ${errorText}`);
1058
- }
1059
-
1060
- async createAssets(twinId: string, assets: AssetPost[], signal?: AbortSignal): Promise<AssetGet[]> {
1061
- const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets`, {
1062
- headers: { 'Content-Type': 'application/json' },
1063
- body: JSON.stringify(assets),
1064
- signal
1065
- });
1066
-
1067
- if (response.status === 201) {
1068
- return (await response.json()) as AssetGet[];
1069
- }
1070
-
1071
- if (response.status === 400) {
1072
- const errorBody = await response.json();
1073
- throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
1074
- }
1075
-
1076
- const errorText = await response.text();
1077
- throw new Error(`Failed to create assets: ${response.status} ${response.statusText} - ${errorText}`);
1078
- }
1079
-
1080
- async updateAsset(twinId: string, assetId: string, asset: AssetPut, signal?: AbortSignal): Promise<AssetGet> {
1081
- const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
1082
- headers: { 'Content-Type': 'application/json' },
1083
- body: JSON.stringify(asset),
1084
- signal
1085
- });
1086
-
1087
- if (response.ok) {
1088
- return (await response.json()) as AssetGet;
1089
- }
1090
-
1091
- if (response.status === 409) {
1092
- const conflictedAsset = (await response.json()) as AssetGet;
1093
- throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
1094
- }
1095
-
1096
- if (response.status === 400) {
1097
- const errorBody = await response.json();
1098
- throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
1099
- }
1100
-
1101
- const errorText = await response.text();
1102
- throw new Error(`Failed to update asset: ${response.status} ${response.statusText} - ${errorText}`);
1103
- }
1104
-
1105
- async deleteAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet> {
1106
- const response = await this._httpClient.fetch(
1107
- HttpMethod.Delete,
1108
- `${this._baseUrl}/${twinId}/assets/${assetId}`,
1109
- {
1110
- headers: { 'Content-Type': 'application/json' },
1111
- body: JSON.stringify({ etag }),
1112
- signal
1113
- }
1114
- );
1115
-
1116
- if (response.ok) {
1117
- return (await response.json()) as AssetGet;
1118
- }
1119
-
1120
- if (response.status === 409) {
1121
- const conflictedAsset = (await response.json()) as AssetGet;
1122
- throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
1123
- }
1124
-
1125
- if (response.status === 404) {
1126
- throw new AssetNotFoundError(`Asset ${assetId} not found`);
1127
- }
1128
-
1129
- const errorText = await response.text();
1130
- throw new Error(`Failed to delete asset: ${response.status} ${response.statusText} - ${errorText}`);
1131
- }
1132
-
1133
- async restoreAsset(twinId: string, assetId: string, etag: string): Promise<AssetGet> {
1134
- const response = await this._httpClient.fetch(
1135
- HttpMethod.Put,
1136
- `${this._baseUrl}/${twinId}/assets/${assetId}/restore`,
1137
- {
1138
- headers: { 'Content-Type': 'application/json' },
1139
- body: JSON.stringify({ etag })
1140
- }
1141
- );
1142
-
1143
- if (response.ok) {
1144
- return (await response.json()) as AssetGet;
1145
- }
1146
-
1147
- if (response.status === 409) {
1148
- const conflictedAsset = (await response.json()) as AssetGet;
1149
- throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
1150
- }
1151
-
1152
- if (response.status === 404) {
1153
- throw new AssetNotFoundError(`Asset ${assetId} not found`);
1154
- }
1155
-
1156
- const errorText = await response.text();
1157
- throw new Error(`Failed to restore asset: ${response.status} ${response.statusText} - ${errorText}`);
1158
- }
1159
-
1160
- async batchAssets(
1161
- twinId: string,
1162
- operations: AssetBatchOperation[],
1163
- signal?: AbortSignal
1164
- ): Promise<AssetBatchResponse> {
1165
- const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch`, {
1166
- headers: { 'Content-Type': 'application/json' },
1167
- body: JSON.stringify({ operations }),
1168
- signal
1169
- });
1170
-
1171
- if (response.ok) {
1172
- return (await response.json()) as AssetBatchResponse;
1173
- }
1174
-
1175
- if (response.status === 400) {
1176
- const errorBody = await response.json();
1177
- throw new AssetValidationError(`Asset batch validation failed: ${errorBody.detail}`, errorBody);
1178
- }
1179
-
1180
- if (response.status === 403) {
1181
- const errorText = await response.text();
1182
- throw new AssetForbiddenError(`Asset batch forbidden: ${errorText}`);
1183
- }
1184
-
1185
- if (response.status === 404 || response.status === 409) {
1186
- const batchResponse = (await response.json()) as AssetBatchResponse;
1187
- throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, batchResponse);
1188
- }
1189
-
1190
- const errorText = await response.text();
1191
- throw new Error(`Failed to execute asset batch: ${response.status} ${response.statusText} - ${errorText}`);
1192
- }
1193
-
1194
- async batchGetAssets(twinId: string, ids: string[], signal?: AbortSignal): Promise<AssetBatchGetResponse> {
1195
- const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch-get`, {
1196
- headers: { 'Content-Type': 'application/json' },
1197
- body: JSON.stringify({ ids }),
1198
- signal
1199
- });
1200
-
1201
- if (response.ok) {
1202
- return (await response.json()) as AssetBatchGetResponse;
1203
- }
1204
-
1205
- if (response.status === 400) {
1206
- const errorBody = await response.json();
1207
- throw new AssetValidationError(`Asset batch-get validation failed: ${errorBody.detail}`, errorBody);
1208
- }
1209
-
1210
- const errorText = await response.text();
1211
- throw new Error(`Failed to batch-get assets: ${response.status} ${response.statusText} - ${errorText}`);
1212
- }
1213
-
1214
- async listTwinFiles(
1215
- twinId: string,
1216
- options: TwinFileListOptions = {},
1217
- signal?: AbortSignal
1218
- ): Promise<PaginatedResponse<TwinFileGet>> {
1219
- const { includeDeleted = false, checkedIn = false, version, page = undefined, limit = 100 } = options;
1220
-
1221
- if (version !== undefined && !checkedIn) {
1222
- throw new Error('The "version" option can only be used when "checkedIn" is true.');
1223
- }
1224
-
1225
- const params = new URLSearchParams();
1226
- if (includeDeleted) params.set('includeDeleted', 'true');
1227
- if (checkedIn) params.set('checkedIn', 'true');
1228
- if (version !== undefined) params.set('version', version.toString());
1229
- if (page !== undefined) params.set('page', page);
1230
- if (limit !== 100) params.set('limit', limit.toString());
1231
-
1232
- const url = params.toString()
1233
- ? `${this._baseUrl}/${twinId}/files?${params}`
1234
- : `${this._baseUrl}/${twinId}/files`;
1235
-
1236
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1237
- headers: { 'Content-Type': 'application/json' },
1238
- signal
1239
- });
1240
-
1241
- if (response.ok) {
1242
- return (await response.json()) as PaginatedResponse<TwinFileGet>;
1243
- }
1244
-
1245
- if (response.status === 400) {
1246
- const errorBody = await response.json();
1247
- throw new TwinFileValidationError(`Twin file listing validation failed: ${errorBody.detail}`, errorBody);
1248
- }
1249
-
1250
- const errorText = await response.text();
1251
- throw new Error(`Failed to list twin files: ${response.status} ${response.statusText} - ${errorText}`);
1252
- }
1253
-
1254
- async getTwinFile(twinId: string, fileId: string, signal?: AbortSignal): Promise<TwinFileGet> {
1255
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1256
- headers: { 'Content-Type': 'application/json' },
1257
- signal
1258
- });
1259
-
1260
- if (response.ok) {
1261
- return (await response.json()) as TwinFileGet;
1262
- }
1263
-
1264
- if (response.status === 404) {
1265
- throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1266
- }
1267
-
1268
- if (response.status === 400) {
1269
- const errorText = await response.text();
1270
- throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1271
- }
1272
-
1273
- const errorText = await response.text();
1274
- throw new Error(`Failed to get twin file: ${response.status} ${response.statusText} - ${errorText}`);
1275
- }
1276
-
1277
- async createOrUpdateTwinFile(
1278
- twinId: string,
1279
- fileId: string,
1280
- twinFile: TwinFilePut,
1281
- signal?: AbortSignal
1282
- ): Promise<TwinFileGet> {
1283
- const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1284
- headers: { 'Content-Type': 'application/json' },
1285
- body: JSON.stringify(twinFile),
1286
- signal
1287
- });
1288
-
1289
- if (response.ok || response.status === 201) {
1290
- return (await response.json()) as TwinFileGet;
1291
- }
1292
-
1293
- if (response.status === 400) {
1294
- const errorBody = await response.json();
1295
- throw new TwinFileValidationError(`Twin file validation failed`, errorBody);
1296
- }
1297
-
1298
- if (response.status === 409) {
1299
- const conflictedFile = (await response.json()) as TwinFileGet;
1300
- throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1301
- }
1302
-
1303
- const errorText = await response.text();
1304
- throw new Error(`Failed to create/update twin file: ${response.status} ${response.statusText} - ${errorText}`);
1305
- }
1306
-
1307
- async deleteTwinFile(
1308
- twinId: string,
1309
- fileId: string,
1310
- deleteRequest: TwinFileDelete,
1311
- signal?: AbortSignal
1312
- ): Promise<TwinFileGet> {
1313
- const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1314
- headers: { 'Content-Type': 'application/json' },
1315
- body: JSON.stringify(deleteRequest),
1316
- signal
1317
- });
1318
-
1319
- if (response.ok) {
1320
- return (await response.json()) as TwinFileGet;
1321
- }
1322
-
1323
- if (response.status === 404) {
1324
- throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1325
- }
1326
-
1327
- if (response.status === 409) {
1328
- const conflictedFile = (await response.json()) as TwinFileGet;
1329
- throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1330
- }
1331
-
1332
- if (response.status === 400) {
1333
- const errorText = await response.text();
1334
- throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1335
- }
1336
-
1337
- const errorText = await response.text();
1338
- throw new Error(`Failed to delete twin file: ${response.status} ${response.statusText} - ${errorText}`);
1339
- }
1340
-
1341
- async restoreTwinFile(twinId: string, fileId: string, etag: string, signal?: AbortSignal): Promise<TwinFileGet> {
1342
- const response = await this._httpClient.fetch(
1343
- HttpMethod.Put,
1344
- `${this._baseUrl}/${twinId}/files/${fileId}/restore`,
1345
- {
1346
- headers: { 'Content-Type': 'application/json' },
1347
- body: JSON.stringify({ etag }),
1348
- signal
1349
- }
1350
- );
1351
-
1352
- if (response.ok) {
1353
- return (await response.json()) as TwinFileGet;
1354
- }
1355
-
1356
- if (response.status === 404) {
1357
- throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1358
- }
1359
-
1360
- if (response.status === 409) {
1361
- const conflictedFile = (await response.json()) as TwinFileGet;
1362
- throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1363
- }
1364
-
1365
- if (response.status === 400) {
1366
- const errorText = await response.text();
1367
- throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1368
- }
1369
-
1370
- const errorText = await response.text();
1371
- throw new Error(`Failed to restore twin file: ${response.status} ${response.statusText} - ${errorText}`);
1372
- }
1373
-
1374
- async getTwinFileContent(twinId: string, fileId: string, signal?: AbortSignal): Promise<Blob> {
1375
- const response = await this._httpClient.fetch(
1376
- HttpMethod.Get,
1377
- `${this._baseUrl}/${twinId}/files/${fileId}/content`,
1378
- { signal }
1379
- );
1380
-
1381
- if (response.ok) {
1382
- return await response.blob();
1383
- }
1384
-
1385
- if (response.status === 404) {
1386
- throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1387
- }
1388
-
1389
- const errorText = await response.text();
1390
- throw new Error(`Failed to get twin file content: ${response.status} ${response.statusText} - ${errorText}`);
1391
- }
1392
-
1393
- async getTwinFileVersion(
1394
- twinId: string,
1395
- fileId: string,
1396
- version: number,
1397
- signal?: AbortSignal
1398
- ): Promise<TwinFileGet> {
1399
- const response = await this._httpClient.fetch(
1400
- HttpMethod.Get,
1401
- `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}`,
1402
- { signal }
1403
- );
1404
-
1405
- if (response.ok) {
1406
- return (await response.json()) as TwinFileGet;
1407
- }
1408
-
1409
- if (response.status === 404) {
1410
- throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} not found`);
1411
- }
1412
-
1413
- const errorText = await response.text();
1414
- throw new Error(`Failed to get twin file version: ${response.status} ${response.statusText} - ${errorText}`);
1415
- }
1416
-
1417
- async listTwinFileVersions(
1418
- twinId: string,
1419
- fileId: string,
1420
- options?: TwinFileVersionListOptions,
1421
- signal?: AbortSignal
1422
- ): Promise<PaginatedResponse<TwinFileGet>> {
1423
- const params = new URLSearchParams();
1424
- if (options?.includeDeleted !== undefined) params.append('includeDeleted', String(options.includeDeleted));
1425
- if (options?.offset !== undefined) params.append('offset', String(options.offset));
1426
- if (options?.limit !== undefined) params.append('limit', String(options.limit));
1427
-
1428
- const response = await this._httpClient.fetch(
1429
- HttpMethod.Get,
1430
- `${this._baseUrl}/${twinId}/files/${fileId}/versions?${params.toString()}`,
1431
- { signal }
1432
- );
1433
-
1434
- if (response.ok) {
1435
- return (await response.json()) as PaginatedResponse<TwinFileGet>;
1436
- }
1437
-
1438
- if (response.status === 400) {
1439
- const errorBody = await response.json();
1440
- throw new TwinFileValidationError(
1441
- `Twin file version listing validation failed: ${errorBody.detail}`,
1442
- errorBody
1443
- );
1444
- }
1445
-
1446
- const errorText = await response.text();
1447
- throw new Error(`Failed to list twin file versions: ${response.status} ${response.statusText} - ${errorText}`);
1448
- }
1449
-
1450
- async getTwinFileVersionContent(
1451
- twinId: string,
1452
- fileId: string,
1453
- version: number,
1454
- signal?: AbortSignal
1455
- ): Promise<Blob> {
1456
- const response = await this._httpClient.fetch(
1457
- HttpMethod.Get,
1458
- `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}/content`,
1459
- { signal }
1460
- );
1461
-
1462
- if (response.ok) {
1463
- return await response.blob();
1464
- }
1465
-
1466
- if (response.status === 404) {
1467
- throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} content not found`);
1468
- }
1469
-
1470
- const errorText = await response.text();
1471
- throw new Error(
1472
- `Failed to get twin file version content: ${response.status} ${response.statusText} - ${errorText}`
1473
- );
1474
- }
1475
-
1476
- async createOrUpdateTwinRaw(twinId: string, twin: unknown, signal?: AbortSignal): Promise<Response> {
1477
- return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
1478
- headers: { 'Content-Type': 'application/json' },
1479
- body: JSON.stringify(twin),
1480
- signal
1481
- });
1482
- }
1483
-
1484
- async createOrUpdateTwinFileRaw(
1485
- twinId: string,
1486
- fileId: string,
1487
- twinFile: unknown,
1488
- signal?: AbortSignal
1489
- ): Promise<Response> {
1490
- return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1491
- headers: { 'Content-Type': 'application/json' },
1492
- body: JSON.stringify(twinFile),
1493
- signal
1494
- });
1495
- }
1496
- }
1
+ import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
2
+ import {
3
+ readT8ClientModelWithFactories,
4
+ readT8ClientModelWithGeometry,
5
+ readT8GIModelWithFactories,
6
+ T8ClientEntityTypeDTO,
7
+ T8ClientModel,
8
+ T8ClientModelDTO,
9
+ T8ClientProductConnectionsDTO,
10
+ T8ClientProductDTO,
11
+ T8ClientProjectConnectionsDTO,
12
+ T8ClientProjectDTO,
13
+ T8ClientUnitDTO,
14
+ T8EntityType,
15
+ T8GIMeshDescriptor,
16
+ T8GIMeshDescriptorDTO,
17
+ T8GIStyle,
18
+ T8GIStyleDTO,
19
+ T8GIRegion,
20
+ T8GIRegionDTO,
21
+ T8Product,
22
+ T8ProductWithGeometry,
23
+ T8Project,
24
+ T8Unit
25
+ } from '@twinfinity/t8-client-model';
26
+ import type { T8ProjectWithGeometry } from '@twinfinity/t8-client-model';
27
+ import type { T8GIModelGeneric } from '@twinfinity/t8-client-model';
28
+ import type {
29
+ PaginatedResponse,
30
+ ListTwinItem,
31
+ TwinGet,
32
+ TwinPut,
33
+ TwinVersionContentResponse,
34
+ TwinProcessorEvent,
35
+ TwinDiagnosticsResponse,
36
+ AssetListItem,
37
+ AssetGet,
38
+ AssetPost,
39
+ AssetPut,
40
+ AssetBatchOperation,
41
+ AssetBatchResponse,
42
+ AssetBatchGetResponse,
43
+ TwinFileGet,
44
+ TwinFilePut,
45
+ TwinFileDelete,
46
+ TwinFileListOptions,
47
+ TwinFileVersionListOptions,
48
+ TwinContentResponse
49
+ } from './types';
50
+ import {
51
+ TwinValidationError,
52
+ TwinConflictError,
53
+ TwinNotFoundError,
54
+ ProcessorNotFoundError,
55
+ AssetValidationError,
56
+ AssetForbiddenError,
57
+ AssetConflictError,
58
+ AssetNotFoundError,
59
+ AssetAttachmentConflictError,
60
+ AssetAttachmentNotFoundError,
61
+ AssetBatchError,
62
+ TwinFileValidationError,
63
+ TwinFileConflictError,
64
+ TwinFileNotFoundError,
65
+ AssetCategoryValidationError,
66
+ AssetCategoryConflictError,
67
+ AssetCategoryNotFoundError,
68
+ AssetCategoryForbiddenError,
69
+ AssetTypeValidationError,
70
+ AssetTypeConflictError,
71
+ AssetTypeNotFoundError,
72
+ AssetTypeForbiddenError,
73
+ AssetCatalogTooLargeError,
74
+ isAssetCatalogTooLargeErrorBody,
75
+ TwinUnauthenticatedError,
76
+ readErrorBody
77
+ } from './errors';
78
+ import type {
79
+ AssetCategoryGet,
80
+ AssetCategoryPut,
81
+ AssetCatalogResponse,
82
+ AssetTypeGet,
83
+ AssetTypePut
84
+ } from './types/asset-types-api';
85
+ import type {
86
+ AttachmentGet,
87
+ AttachmentPost,
88
+ AssetAttachmentPut,
89
+ ApiCreateUploadSessionResponse,
90
+ UploadStatusGet
91
+ } from './types/asset-attachment-api';
92
+ import type { MyPermissions } from './types/twin-permissions-api';
93
+ import { Twin } from './types/Twin';
94
+ import { TwinGeometryInfo } from './types/TwinGeometryInfo';
95
+
96
+ export interface ITwinClient {
97
+ /**
98
+ * @param relatedTo - Filter twins by container ID.
99
+ * @throws {TwinValidationError} On invalid query parameters (400).
100
+ */
101
+ listTwins(
102
+ relatedTo?: string,
103
+ page?: string,
104
+ limit?: number,
105
+ signal?: AbortSignal
106
+ ): Promise<PaginatedResponse<ListTwinItem>>;
107
+
108
+ /**
109
+ * Fetches all twins by auto-paginating through {@link listTwins}. Caps at 1000 pages.
110
+ * @param relatedTo - Filter twins by container ID.
111
+ */
112
+ listAllTwins(relatedTo?: string, signal?: AbortSignal): Promise<ListTwinItem[]>;
113
+
114
+ /**
115
+ * @param twin - Twin ID string, or `{id, version}` to fetch a specific version.
116
+ * @throws {TwinNotFoundError} When the twin or version does not exist (404).
117
+ */
118
+ getTwin(twin: string | { id: string; version: number }, signal?: AbortSignal): Promise<TwinGet>;
119
+
120
+ /**
121
+ * @throws {TwinValidationError} On invalid query parameters (400).
122
+ */
123
+ listTwinVersions(
124
+ id: string,
125
+ options?: {
126
+ includeDeleted?: boolean;
127
+ page?: string;
128
+ limit?: number;
129
+ },
130
+ signal?: AbortSignal
131
+ ): Promise<PaginatedResponse<ListTwinItem>>;
132
+
133
+ downloadTwinContent(
134
+ twinId: string,
135
+ twinVersion: number,
136
+ content: TwinContentResponse,
137
+ signal?: AbortSignal
138
+ ): Promise<ArrayBuffer>;
139
+
140
+ /**
141
+ * Downloads and deserializes the T8 client model (t8cm + t8cm-ext) using default types.
142
+ * When no version is specified, searches versions newest-first for one containing t8cm content.
143
+ *
144
+ * @param twin - Twin ID string, or `{id, version}` to load a specific version.
145
+ * @throws {Error} When no version has t8cm content.
146
+ */
147
+ loadTwin(
148
+ twin: string | { id: string; version: number },
149
+ signal?: AbortSignal
150
+ ): Promise<Twin<T8Project, T8Product<T8EntityType>, T8EntityType, T8Unit>>;
151
+
152
+ /**
153
+ * Downloads and deserializes the T8 client model using custom factory functions for domain-specific types.
154
+ * When no version is specified, searches versions newest-first for one containing t8cm content.
155
+ *
156
+ * @param twin - Twin ID string, or `{id, version}` to load a specific version.
157
+ * @throws {Error} When no version has t8cm content.
158
+ */
159
+ loadTwinWithFactories<TProject, TProduct, TEntityType, TUnit>(
160
+ twin: string | { id: string; version: number },
161
+ projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
162
+ productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
163
+ entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
164
+ unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
165
+ connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
166
+ connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
167
+ signal?: AbortSignal
168
+ ): Promise<Twin<TProject, TProduct, TEntityType, TUnit>>;
169
+
170
+ /**
171
+ * Downloads and deserializes geometry info (t8gi) using default types.
172
+ * When no version is specified, searches versions newest-first for one containing t8gi content.
173
+ *
174
+ * @param twin - Twin ID string, or `{id, version}` to load a specific version.
175
+ * @throws {Error} When no version has t8gi content.
176
+ */
177
+ loadGeometryInfo(
178
+ twin: string | { id: string; version: number },
179
+ signal?: AbortSignal
180
+ ): Promise<TwinGeometryInfo<T8GIMeshDescriptor, T8GIStyle, T8GIRegion>>;
181
+
182
+ /**
183
+ * Downloads and deserializes geometry info (t8gi) using custom factory functions.
184
+ * When no version is specified, searches versions newest-first for one containing t8gi content.
185
+ *
186
+ * @param twin - Twin ID string, or `{id, version}` to load a specific version.
187
+ * @throws {Error} When no version has t8gi content.
188
+ */
189
+ loadGeometryInfoWithFactories<TMeshDescriptor, TStyle, TRegion>(
190
+ twin: string | { id: string; version: number },
191
+ meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
192
+ styleFactory: (dto: T8GIStyleDTO) => TStyle,
193
+ regionFactory: (dto: T8GIRegionDTO) => TRegion,
194
+ signal?: AbortSignal
195
+ ): Promise<TwinGeometryInfo<TMeshDescriptor, TStyle, TRegion>>;
196
+
197
+ /**
198
+ * Downloads and deserializes both the client model (t8cm) and geometry (t8gi/t8gd) into a combined result.
199
+ * When no version is specified, searches versions newest-first for one containing both t8cm and t8gi content.
200
+ *
201
+ * @param twin - Twin ID string, or `{id, version}` to load a specific version.
202
+ * @throws {Error} When no version has both t8cm and t8gi content.
203
+ */
204
+ loadTwinWithGeometry(
205
+ twin: string | { id: string; version: number },
206
+ signal?: AbortSignal
207
+ ): Promise<Twin<T8ProjectWithGeometry, T8ProductWithGeometry, T8EntityType, T8Unit>>;
208
+
209
+ /**
210
+ * Creates a new twin or updates an existing one (upsert). Provide `etag` in the twin body to update.
211
+ *
212
+ * @throws {TwinValidationError} On invalid request body (400).
213
+ * @throws {TwinConflictError} On etag mismatch (409). The error's `currentTwin` contains the server's current state.
214
+ */
215
+ createOrUpdateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet>;
216
+
217
+ /** Alias for {@link createOrUpdateTwin}. */
218
+ updateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet>;
219
+
220
+ /**
221
+ * @throws {TwinNotFoundError} When the twin does not exist (404).
222
+ * @throws {TwinConflictError} On etag mismatch (409). The error's `currentTwin` contains the server's current state.
223
+ */
224
+ restoreTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet>;
225
+
226
+ /**
227
+ * @throws {TwinNotFoundError} When the twin or version does not exist (404).
228
+ */
229
+ getTwinVersionContent(
230
+ twinId: string,
231
+ twinVersion: number,
232
+ signal?: AbortSignal
233
+ ): Promise<TwinVersionContentResponse>;
234
+
235
+ /**
236
+ * Returns content for the latest twin version.
237
+ * @throws {TwinNotFoundError} When the twin does not exist (404).
238
+ */
239
+ getTwinContent(twinId: string, signal?: AbortSignal): Promise<TwinVersionContentResponse>;
240
+
241
+ /** @throws {TwinNotFoundError} When the twin does not exist (404). */
242
+ getTwinDiagnostics(twinId: string, signal?: AbortSignal): Promise<TwinDiagnosticsResponse>;
243
+
244
+ /** @throws {TwinNotFoundError} When the twin or version does not exist (404). */
245
+ getTwinVersionDiagnostics(
246
+ twinId: string,
247
+ twinVersion: number,
248
+ signal?: AbortSignal
249
+ ): Promise<TwinDiagnosticsResponse>;
250
+
251
+ /**
252
+ * @throws {TwinValidationError} On invalid query parameters (400).
253
+ * @throws {TwinNotFoundError} When the twin or version does not exist (404).
254
+ */
255
+ getTwinVersionEvents(
256
+ twinId: string,
257
+ twinVersion: number,
258
+ offset?: number,
259
+ limit?: number,
260
+ signal?: AbortSignal
261
+ ): Promise<PaginatedResponse<TwinProcessorEvent>>;
262
+
263
+ /**
264
+ * @throws {TwinValidationError} On invalid query parameters (400).
265
+ * @throws {ProcessorNotFoundError} When the processor does not exist for this twin version (404).
266
+ */
267
+ getProcessorEvents(
268
+ twinId: string,
269
+ twinVersion: number,
270
+ processorId: string,
271
+ offset?: number,
272
+ limit?: number,
273
+ signal?: AbortSignal
274
+ ): Promise<PaginatedResponse<TwinProcessorEvent>>;
275
+
276
+ /**
277
+ * @throws {AssetValidationError} On invalid query parameters (400).
278
+ * @throws {AssetNotFoundError} When the twin does not exist (404).
279
+ */
280
+ listAssets(
281
+ twinId: string,
282
+ offset?: number,
283
+ limit?: number,
284
+ options?: { includeDeleted?: boolean },
285
+ signal?: AbortSignal
286
+ ): Promise<PaginatedResponse<AssetListItem>>;
287
+
288
+ /** @throws {AssetNotFoundError} When the asset does not exist (404). */
289
+ getAsset(twinId: string, assetId: string, signal?: AbortSignal): Promise<AssetGet>;
290
+
291
+ /** @throws {AssetValidationError} On invalid request body (400). The error's `errorBody` has structured detail. */
292
+ createAssets(twinId: string, assets: AssetPost[], signal?: AbortSignal): Promise<AssetGet[]>;
293
+
294
+ /**
295
+ * @throws {AssetValidationError} On invalid request body (400). The error's `errorBody` has structured detail.
296
+ * @throws {AssetConflictError} On etag mismatch (409). The error's `currentAsset` contains the server's current state.
297
+ */
298
+ updateAsset(twinId: string, assetId: string, asset: AssetPut, signal?: AbortSignal): Promise<AssetGet>;
299
+
300
+ /**
301
+ * @throws {AssetNotFoundError} When the asset does not exist (404).
302
+ * @throws {AssetConflictError} On etag mismatch (409). The error's `currentAsset` contains the server's current state.
303
+ */
304
+ deleteAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet>;
305
+
306
+ /**
307
+ * @throws {AssetNotFoundError} When the asset does not exist (404).
308
+ * @throws {AssetConflictError} On etag mismatch (409). The error's `currentAsset` contains the server's current state.
309
+ */
310
+ restoreAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet>;
311
+
312
+ /**
313
+ * Executes a batch of asset operations (upsert, delete, restore) atomically.
314
+ * All operations succeed or all are rolled back.
315
+ *
316
+ * @param operations - Array of batch operations (max 1000).
317
+ * @throws {AssetValidationError} On validation failure (400).
318
+ * @throws {AssetForbiddenError} On insufficient permissions (403).
319
+ * @throws {AssetBatchError} On conflict (409) or not found (404). The error's `response` has per-item results with reverted statuses.
320
+ */
321
+ batchAssets(twinId: string, operations: AssetBatchOperation[], signal?: AbortSignal): Promise<AssetBatchResponse>;
322
+
323
+ /**
324
+ * Retrieves multiple assets by ID in a single request.
325
+ *
326
+ * @param ids - Array of asset IDs (max 1000).
327
+ * @throws {AssetValidationError} On validation failure (400).
328
+ */
329
+ batchGetAssets(twinId: string, ids: string[], signal?: AbortSignal): Promise<AssetBatchGetResponse>;
330
+
331
+ /** Lists attachments for a single asset. */
332
+ listAssetAttachments(
333
+ twinId: string,
334
+ assetId: string,
335
+ options?: { includeDeleted?: boolean; offset?: number; limit?: number; page?: string },
336
+ signal?: AbortSignal
337
+ ): Promise<PaginatedResponse<AttachmentGet>>;
338
+
339
+ /**
340
+ * Downloads an attachment's blob as a `Response`. The caller decides
341
+ * whether to consume it as text, ArrayBuffer, or Blob.
342
+ * @throws {AssetAttachmentNotFoundError} When the attachment does not exist (404).
343
+ */
344
+ getAssetAttachmentBlob(
345
+ twinId: string,
346
+ assetId: string,
347
+ attachmentId: string,
348
+ signal?: AbortSignal
349
+ ): Promise<Response>;
350
+
351
+ /**
352
+ * Creates an upload session for a new attachment. The actual file bytes
353
+ * are uploaded out-of-band to {@link ApiCreateUploadSessionResponse.uploadUrl};
354
+ * use {@link uploadAssetAttachmentFile} for the canonical chunked-upload flow.
355
+ * The attachment row is materialized server-side when the blob lands.
356
+ * @throws {AssetAttachmentConflictError} When an attachment already exists
357
+ * for the given id (409); retries must use a new attachmentId.
358
+ */
359
+ createAssetAttachmentUploadSession(
360
+ twinId: string,
361
+ assetId: string,
362
+ attachmentId: string,
363
+ body: AttachmentPost,
364
+ signal?: AbortSignal
365
+ ): Promise<ApiCreateUploadSessionResponse>;
366
+
367
+ /**
368
+ * Uploads a `File` / `Blob` to an existing upload session in one or more
369
+ * chunks, then resolves the upload token so the caller can poll
370
+ * {@link getAssetAttachmentUploadStatus}.
371
+ */
372
+ uploadAssetAttachmentFile(
373
+ session: ApiCreateUploadSessionResponse,
374
+ file: Blob,
375
+ signal?: AbortSignal
376
+ ): Promise<{ token: string }>;
377
+
378
+ /** Polls the in-memory upload-session cache for an attachment that's being uploaded. */
379
+ getAssetAttachmentUploadStatus(
380
+ twinId: string,
381
+ assetId: string,
382
+ token: string,
383
+ signal?: AbortSignal
384
+ ): Promise<UploadStatusGet>;
385
+
386
+ /**
387
+ * Updates an attachment's description / soft-delete flag. `documentId`
388
+ * is immutable — rebinding is rejected.
389
+ * @throws {AssetAttachmentConflictError} On etag mismatch (409). The
390
+ * error's `currentAttachment` carries the server's current state.
391
+ * @throws {AssetAttachmentNotFoundError} When the attachment does not exist (404).
392
+ */
393
+ putAssetAttachment(
394
+ twinId: string,
395
+ assetId: string,
396
+ attachmentId: string,
397
+ body: AssetAttachmentPut,
398
+ signal?: AbortSignal
399
+ ): Promise<AttachmentGet>;
400
+
401
+ /**
402
+ * Soft-deletes an attachment.
403
+ * @throws {AssetAttachmentConflictError} On etag mismatch (409).
404
+ * @throws {AssetAttachmentNotFoundError} When the attachment does not exist (404).
405
+ */
406
+ deleteAssetAttachment(
407
+ twinId: string,
408
+ assetId: string,
409
+ attachmentId: string,
410
+ etag: string,
411
+ signal?: AbortSignal
412
+ ): Promise<AttachmentGet>;
413
+
414
+ /**
415
+ * Restores a soft-deleted attachment.
416
+ * @throws {AssetAttachmentConflictError} On etag mismatch (409).
417
+ * @throws {AssetAttachmentNotFoundError} When the attachment does not exist (404).
418
+ */
419
+ restoreAssetAttachment(
420
+ twinId: string,
421
+ assetId: string,
422
+ attachmentId: string,
423
+ etag: string,
424
+ signal?: AbortSignal
425
+ ): Promise<AttachmentGet>;
426
+
427
+ /**
428
+ * @throws {TwinNotFoundError} When the twin does not exist (404).
429
+ * @throws {TwinConflictError} On etag mismatch (409). The error's `currentTwin` contains the server's current state.
430
+ */
431
+ deleteTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet>;
432
+
433
+ /** @throws {TwinFileValidationError} On invalid query parameters (400). */
434
+ listTwinFiles(
435
+ twinId: string,
436
+ options?: TwinFileListOptions,
437
+ signal?: AbortSignal
438
+ ): Promise<PaginatedResponse<TwinFileGet>>;
439
+
440
+ /**
441
+ * @throws {TwinFileNotFoundError} When the file does not exist (404).
442
+ * @throws {TwinFileValidationError} On invalid request (400).
443
+ */
444
+ getTwinFile(twinId: string, fileId: string, signal?: AbortSignal): Promise<TwinFileGet>;
445
+
446
+ /**
447
+ * @throws {TwinFileValidationError} On invalid request body (400).
448
+ * @throws {TwinFileConflictError} On etag mismatch (409). The error's `currentTwinFile` contains the server's current state.
449
+ */
450
+ createOrUpdateTwinFile(
451
+ twinId: string,
452
+ fileId: string,
453
+ twinFile: TwinFilePut,
454
+ signal?: AbortSignal
455
+ ): Promise<TwinFileGet>;
456
+
457
+ /**
458
+ * @throws {TwinFileNotFoundError} When the file does not exist (404).
459
+ * @throws {TwinFileConflictError} On etag mismatch (409). The error's `currentTwinFile` contains the server's current state.
460
+ * @throws {TwinFileValidationError} On invalid request (400).
461
+ */
462
+ deleteTwinFile(
463
+ twinId: string,
464
+ fileId: string,
465
+ deleteRequest: TwinFileDelete,
466
+ signal?: AbortSignal
467
+ ): Promise<TwinFileGet>;
468
+
469
+ /**
470
+ * @throws {TwinFileNotFoundError} When the file does not exist (404).
471
+ * @throws {TwinFileConflictError} On etag mismatch (409). The error's `currentTwinFile` contains the server's current state.
472
+ * @throws {TwinFileValidationError} On invalid request (400).
473
+ */
474
+ restoreTwinFile(twinId: string, fileId: string, etag: string, signal?: AbortSignal): Promise<TwinFileGet>;
475
+
476
+ /** @throws {TwinFileNotFoundError} When the file does not exist (404). */
477
+ getTwinFileContent(twinId: string, fileId: string, signal?: AbortSignal): Promise<Blob>;
478
+
479
+ /** @throws {TwinFileNotFoundError} When the file or version does not exist (404). */
480
+ getTwinFileVersion(twinId: string, fileId: string, version: number, signal?: AbortSignal): Promise<TwinFileGet>;
481
+
482
+ /** @throws {TwinFileValidationError} On invalid query parameters (400). */
483
+ listTwinFileVersions(
484
+ twinId: string,
485
+ fileId: string,
486
+ options?: TwinFileVersionListOptions,
487
+ signal?: AbortSignal
488
+ ): Promise<PaginatedResponse<TwinFileGet>>;
489
+
490
+ /** @throws {TwinFileNotFoundError} When the file or version does not exist (404). */
491
+ getTwinFileVersionContent(twinId: string, fileId: string, version: number, signal?: AbortSignal): Promise<Blob>;
492
+
493
+ // ---------- Asset categories (TWINS-3643) ----------
494
+
495
+ /**
496
+ * Returns the caller's named, customer-scoped capabilities against the
497
+ * Twin service (e.g. `ViewAssetTypes`, `EditAssetTypes`). The admin app
498
+ * uses this to enable or disable buttons instead of letting the user
499
+ * click through to a 403.
500
+ *
501
+ * No client-side cache — every call hits the wire. Underlying scope
502
+ * grants don't shift within a session, so callers are expected to
503
+ * memoise the result themselves (e.g. via React Query / a once-on-mount
504
+ * fetch) rather than calling this on every render.
505
+ *
506
+ * Throws {@link TwinUnauthenticatedError} on 401 (no valid auth context;
507
+ * caller should redirect to login). Returns an empty `permissions` array
508
+ * on 403 (authenticated but no Twin-service capabilities). Unknown
509
+ * capability strings should be ignored — adding a new capability is a
510
+ * non-breaking change.
511
+ */
512
+ getMyPermissions(signal?: AbortSignal): Promise<MyPermissions>;
513
+
514
+ /**
515
+ * Returns the Twinfinity meta-schema document — the JSON Schema describing
516
+ * valid asset-type metadata schemas. The bytes the backend itself
517
+ * validates against, served verbatim. Power-user tools (Monaco-backed
518
+ * JSON editors, schema-aware tooling) fetch this at runtime so their
519
+ * in-editor diagnostics line up with what the server will accept,
520
+ * decoupled from the FE bundle's build time.
521
+ */
522
+ getAssetTypeMetaSchema(signal?: AbortSignal): Promise<Record<string, unknown>>;
523
+
524
+ /**
525
+ * @throws {AssetCategoryValidationError} On invalid query parameters (400).
526
+ */
527
+ listAssetCategories(
528
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
529
+ signal?: AbortSignal
530
+ ): Promise<PaginatedResponse<AssetCategoryGet>>;
531
+
532
+ /** @throws {AssetCategoryNotFoundError} When the category does not exist (404). */
533
+ getAssetCategory(categoryId: string, signal?: AbortSignal): Promise<AssetCategoryGet>;
534
+
535
+ /**
536
+ * Upserts an asset category. Provide an empty/missing etag for first writes
537
+ * and the existing etag to update.
538
+ *
539
+ * @throws {AssetCategoryValidationError} On invalid request body (400).
540
+ * @throws {AssetCategoryConflictError} On etag mismatch (409). The error's `currentCategory` contains the server's current state.
541
+ */
542
+ putAssetCategory(categoryId: string, body: AssetCategoryPut, signal?: AbortSignal): Promise<AssetCategoryGet>;
543
+
544
+ /**
545
+ * Returns every version of a single asset category, newest first. Mirrors
546
+ * `listAssetTypeVersions` and `twin.get_twin_versions`.
547
+ *
548
+ * @throws {AssetCategoryNotFoundError} When the category's header row does not exist (404).
549
+ */
550
+ listAssetCategoryVersions(
551
+ categoryId: string,
552
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
553
+ signal?: AbortSignal
554
+ ): Promise<PaginatedResponse<AssetCategoryGet>>;
555
+
556
+ /**
557
+ * Soft-deletes a category. Inserts a new asset_category_versions row with
558
+ * `is_deleted=true`; the previous version is retained as an audit trail.
559
+ *
560
+ * @throws {AssetCategoryNotFoundError} When the category does not exist (404).
561
+ * @throws {AssetCategoryConflictError} On etag mismatch (409).
562
+ */
563
+ deleteAssetCategory(categoryId: string, etag: string, signal?: AbortSignal): Promise<AssetCategoryGet>;
564
+
565
+ /**
566
+ * Restores a soft-deleted category. Inserts a new asset_category_versions
567
+ * row with `is_deleted=false`; mirrors twin restore.
568
+ *
569
+ * @throws {AssetCategoryNotFoundError} When the category does not exist (404).
570
+ * @throws {AssetCategoryConflictError} On etag mismatch (409).
571
+ */
572
+ restoreAssetCategory(categoryId: string, etag: string, signal?: AbortSignal): Promise<AssetCategoryGet>;
573
+
574
+ // ---------- Asset types (TWINS-3643) ----------
575
+
576
+ /** @throws {AssetTypeValidationError} On invalid query parameters (400). */
577
+ listAssetTypes(
578
+ categoryId: string,
579
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
580
+ signal?: AbortSignal
581
+ ): Promise<PaginatedResponse<AssetTypeGet>>;
582
+
583
+ /** @throws {AssetTypeNotFoundError} When the category or type does not exist (404). */
584
+ getAssetType(categoryId: string, typeId: string, signal?: AbortSignal): Promise<AssetTypeGet>;
585
+
586
+ /**
587
+ * Returns every version of a single asset type, newest first. Mirrors
588
+ * `twin.get_twin_versions`.
589
+ *
590
+ * @throws {AssetTypeNotFoundError} When the type's header row does not exist (404).
591
+ */
592
+ listAssetTypeVersions(
593
+ categoryId: string,
594
+ typeId: string,
595
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
596
+ signal?: AbortSignal
597
+ ): Promise<PaginatedResponse<AssetTypeGet>>;
598
+
599
+ /**
600
+ * Upserts an asset type. The metadata schema is validated against the
601
+ * Twinfinity meta-schema at the API boundary; invalid schemas surface as
602
+ * a 400 with `cause: MetadataSchemaInvalid`.
603
+ *
604
+ * @throws {AssetTypeValidationError} On invalid request body (400). The error's `errorBody` carries the structured cause.
605
+ * @throws {AssetTypeConflictError} On etag mismatch (409). The error's `currentType` contains the server's current state.
606
+ */
607
+ putAssetType(categoryId: string, typeId: string, body: AssetTypePut, signal?: AbortSignal): Promise<AssetTypeGet>;
608
+
609
+ /**
610
+ * Soft-deletes a type. Inserts a new asset_type_versions row with
611
+ * `is_deleted=true`; the previous version is retained as an audit trail.
612
+ *
613
+ * @throws {AssetTypeNotFoundError} When the type does not exist (404).
614
+ * @throws {AssetTypeConflictError} On etag mismatch (409).
615
+ */
616
+ deleteAssetType(categoryId: string, typeId: string, etag: string, signal?: AbortSignal): Promise<AssetTypeGet>;
617
+
618
+ /**
619
+ * Restores a soft-deleted type. Inserts a new asset_type_versions row with
620
+ * `is_deleted=false`; mirrors twin restore.
621
+ *
622
+ * @throws {AssetTypeNotFoundError} When the type does not exist (404).
623
+ * @throws {AssetTypeConflictError} On etag mismatch (409).
624
+ */
625
+ restoreAssetType(categoryId: string, typeId: string, etag: string, signal?: AbortSignal): Promise<AssetTypeGet>;
626
+
627
+ /**
628
+ * Returns the customer's asset-type catalog: categories nested with
629
+ * their latest non-deleted types and full metadata schemas inline. One
630
+ * round-trip is enough to populate a picker dropdown and render any
631
+ * per-type metadata form.
632
+ */
633
+ getAssetTypeCatalog(signal?: AbortSignal): Promise<AssetCatalogResponse>;
634
+ }
635
+
636
+ export class TwinClient implements ITwinClient {
637
+ private readonly _baseUrl: string;
638
+ private readonly _assetTypeCatalogUrl: string;
639
+
640
+ constructor(
641
+ baseUrl: string | URL,
642
+ private readonly _httpClient: TwinfinityHttpClient
643
+ ) {
644
+ const base = String(baseUrl).replace(/\/+$/, '');
645
+ this._baseUrl = `${base}/_ps/twins`;
646
+ this._assetTypeCatalogUrl = `${base}/_ps/asset-type-catalog`;
647
+ }
648
+
649
+ async listTwins(
650
+ relatedTo?: string,
651
+ page: string | undefined = undefined,
652
+ limit = 100,
653
+ signal?: AbortSignal
654
+ ): Promise<PaginatedResponse<ListTwinItem>> {
655
+ const params = new URLSearchParams();
656
+ if (relatedTo) params.set('relatedTo', relatedTo);
657
+ if (page !== undefined) params.set('page', page);
658
+ if (limit !== 100) params.set('limit', limit.toString());
659
+
660
+ const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
661
+
662
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
663
+ headers: { Accept: 'application/json' },
664
+ signal
665
+ });
666
+
667
+ if (response.ok) {
668
+ return (await response.json()) as PaginatedResponse<ListTwinItem>;
669
+ }
670
+
671
+ if (response.status === 400) {
672
+ const errorBody = await readErrorBody(response);
673
+ throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
674
+ }
675
+
676
+ const errorText = await response.text();
677
+ throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
678
+ }
679
+
680
+ async listAllTwins(relatedTo?: string, signal?: AbortSignal): Promise<ListTwinItem[]> {
681
+ const allTwins: ListTwinItem[] = [];
682
+ let page: string | undefined = undefined;
683
+ const limit = 100;
684
+ const maxPages = 1000;
685
+
686
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
687
+ const response = await this.listTwins(relatedTo, page, limit, signal);
688
+ allTwins.push(...response.data);
689
+
690
+ if (response.data.length < limit) {
691
+ break;
692
+ }
693
+
694
+ page = response.pagination.nextPage;
695
+ }
696
+
697
+ return allTwins;
698
+ }
699
+
700
+ async getTwin(twin: string | { id: string; version: number }, signal?: AbortSignal): Promise<TwinGet> {
701
+ const twinId = typeof twin === 'string' ? twin : twin.id;
702
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
703
+ const response = await this._httpClient.fetch(
704
+ HttpMethod.Get,
705
+ `${this._baseUrl}/${twinId}${specifiedVersion !== undefined ? `/versions/${specifiedVersion}` : ''}`,
706
+ {
707
+ headers: { Accept: 'application/json' },
708
+ signal
709
+ }
710
+ );
711
+
712
+ if (response.ok) {
713
+ return (await response.json()) as TwinGet;
714
+ }
715
+
716
+ if (response.status === 404) {
717
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
718
+ }
719
+
720
+ const errorText = await response.text();
721
+ throw new Error(`Failed to get twin: ${response.status} ${response.statusText} - ${errorText}`);
722
+ }
723
+
724
+ async listTwinVersions(
725
+ id: string,
726
+ options?: {
727
+ includeDeleted?: boolean;
728
+ page: string | undefined;
729
+ limit?: number;
730
+ },
731
+ signal?: AbortSignal
732
+ ): Promise<PaginatedResponse<ListTwinItem>> {
733
+ const params = new URLSearchParams();
734
+ if (options?.page !== undefined) params.set('page', options.page);
735
+ if (options?.limit !== undefined && options.limit !== 100) params.set('limit', options.limit.toString());
736
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
737
+
738
+ const url = `${this._baseUrl}/${id}/versions${params.toString() ? `?${params}` : ''}`;
739
+
740
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
741
+ headers: { Accept: 'application/json' },
742
+ signal
743
+ });
744
+
745
+ if (response.ok) {
746
+ return (await response.json()) as PaginatedResponse<ListTwinItem>;
747
+ }
748
+
749
+ if (response.status === 400) {
750
+ const errorBody = await readErrorBody(response);
751
+ throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
752
+ }
753
+
754
+ const errorText = await response.text();
755
+ throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
756
+ }
757
+
758
+ async downloadTwinContent(
759
+ twinId: string,
760
+ twinVersion: number,
761
+ content: TwinContentResponse,
762
+ signal?: AbortSignal
763
+ ): Promise<ArrayBuffer> {
764
+ const response = await this._httpClient.fetch(
765
+ HttpMethod.Get,
766
+ `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents/${content.id}/content`,
767
+ { signal }
768
+ );
769
+ if (response.ok) {
770
+ const blob = await response.blob();
771
+ return await blob.arrayBuffer();
772
+ }
773
+
774
+ throw new Error(`Failed to download twin content: ${response.status} ${response.statusText}`);
775
+ }
776
+
777
+ private async loadTwinVersion<TProject, TProduct, TEntityType, TUnit>(
778
+ twinId: string,
779
+ twinVersion: number,
780
+ projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
781
+ productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
782
+ entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
783
+ unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
784
+ connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
785
+ connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
786
+ signal?: AbortSignal
787
+ ): Promise<T8ClientModel<TProject, TProduct, TEntityType, TUnit> | undefined> {
788
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
789
+
790
+ const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
791
+
792
+ if (t8cmItem === undefined) {
793
+ return undefined;
794
+ }
795
+
796
+ const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
797
+
798
+ const data = await Promise.all(
799
+ [this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal)].concat(
800
+ t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal))
801
+ )
802
+ );
803
+
804
+ return readT8ClientModelWithFactories<TProject, TProduct, TEntityType, TUnit>(
805
+ data,
806
+ projectFactory,
807
+ productFactory,
808
+ entityTypeFactory,
809
+ unitFactory,
810
+ connectProject,
811
+ connectProduct
812
+ );
813
+ }
814
+
815
+ loadTwin(
816
+ twin: string | { id: string; version: number },
817
+ signal?: AbortSignal
818
+ ): Promise<Twin<T8Project, T8Product<T8EntityType>, T8EntityType, T8Unit>> {
819
+ return this.loadTwinWithFactories(
820
+ twin,
821
+ T8Project.create<T8Product<T8EntityType>, T8Unit>,
822
+ T8Product.create<T8EntityType, T8Unit>,
823
+ T8EntityType.create,
824
+ T8Unit.create,
825
+ T8Project.connect,
826
+ T8Product.connect,
827
+ signal
828
+ );
829
+ }
830
+
831
+ async loadTwinWithFactories<TProject, TProduct, TEntityType, TUnit>(
832
+ twin: string | { id: string; version: number },
833
+ projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
834
+ productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
835
+ entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
836
+ unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
837
+ connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
838
+ connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
839
+ signal?: AbortSignal
840
+ ): Promise<Twin<TProject, TProduct, TEntityType, TUnit>> {
841
+ let page: string | undefined = undefined;
842
+ const twinId = typeof twin === 'string' ? twin : twin.id;
843
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
844
+ if (specifiedVersion !== undefined) {
845
+ return {
846
+ id: twinId,
847
+ version: specifiedVersion,
848
+ model: await this.loadTwinVersion(
849
+ twinId,
850
+ specifiedVersion,
851
+ projectFactory,
852
+ productFactory,
853
+ entityTypeFactory,
854
+ unitFactory,
855
+ connectProject,
856
+ connectProduct,
857
+ signal
858
+ )
859
+ };
860
+ }
861
+
862
+ const maxPages = 100;
863
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
864
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
865
+ if (versions.data.length === 0) {
866
+ break;
867
+ }
868
+ for (const versionItem of versions.data) {
869
+ const loadedModel = await this.loadTwinVersion(
870
+ twinId,
871
+ versionItem.version,
872
+ projectFactory,
873
+ productFactory,
874
+ entityTypeFactory,
875
+ unitFactory,
876
+ connectProject,
877
+ connectProduct,
878
+ signal
879
+ );
880
+ if (loadedModel !== undefined) {
881
+ return {
882
+ id: twinId,
883
+ version: versionItem.version,
884
+ model: loadedModel
885
+ };
886
+ }
887
+ }
888
+ page = versions.pagination.nextPage;
889
+ }
890
+
891
+ throw new Error(`Twin ${twinId} does not have a client model`);
892
+ }
893
+
894
+ private async loadGeometryInfoVersion<TMeshDescriptor, TStyle, TRegion>(
895
+ twinId: string,
896
+ twinVersion: number,
897
+ meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
898
+ styleFactory: (dto: T8GIStyleDTO) => TStyle,
899
+ regionFactory: (dto: T8GIRegionDTO) => TRegion,
900
+ signal?: AbortSignal
901
+ ): Promise<T8GIModelGeneric<TMeshDescriptor, TStyle, TRegion> | undefined> {
902
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
903
+
904
+ const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
905
+
906
+ if (t8giItem === undefined) {
907
+ return undefined;
908
+ }
909
+
910
+ const data = await this.downloadTwinContent(twinId, twinVersion, t8giItem, signal);
911
+
912
+ return readT8GIModelWithFactories(data, meshDescriptorFactory, styleFactory, regionFactory);
913
+ }
914
+
915
+ loadGeometryInfo(
916
+ twin: string | { id: string; version: number },
917
+ signal?: AbortSignal
918
+ ): Promise<TwinGeometryInfo<T8GIMeshDescriptor, T8GIStyle, T8GIRegion>> {
919
+ return this.loadGeometryInfoWithFactories(
920
+ twin,
921
+ T8GIMeshDescriptor.create,
922
+ T8GIStyle.create,
923
+ T8GIRegion.create,
924
+ signal
925
+ );
926
+ }
927
+
928
+ async loadGeometryInfoWithFactories<TMeshDescriptor, TStyle, TRegion>(
929
+ twin: string | { id: string; version: number },
930
+ meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
931
+ styleFactory: (dto: T8GIStyleDTO) => TStyle,
932
+ regionFactory: (dto: T8GIRegionDTO) => TRegion,
933
+ signal?: AbortSignal
934
+ ): Promise<TwinGeometryInfo<TMeshDescriptor, TStyle, TRegion>> {
935
+ let page: string | undefined = undefined;
936
+ const twinId = typeof twin === 'string' ? twin : twin.id;
937
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
938
+
939
+ if (specifiedVersion !== undefined) {
940
+ return {
941
+ id: twinId,
942
+ version: specifiedVersion,
943
+ model: await this.loadGeometryInfoVersion(
944
+ twinId,
945
+ specifiedVersion,
946
+ meshDescriptorFactory,
947
+ styleFactory,
948
+ regionFactory,
949
+ signal
950
+ )
951
+ };
952
+ }
953
+
954
+ const maxPages = 100;
955
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
956
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
957
+ if (versions.data.length === 0) {
958
+ break;
959
+ }
960
+ for (const versionItem of versions.data) {
961
+ const model = await this.loadGeometryInfoVersion(
962
+ twinId,
963
+ versionItem.version,
964
+ meshDescriptorFactory,
965
+ styleFactory,
966
+ regionFactory,
967
+ signal
968
+ );
969
+ if (model !== undefined) {
970
+ return {
971
+ id: twinId,
972
+ version: versionItem.version,
973
+ model
974
+ };
975
+ }
976
+ }
977
+ page = versions.pagination.nextPage;
978
+ }
979
+
980
+ throw new Error(`Twin ${twinId} does not have geometry info`);
981
+ }
982
+
983
+ private async loadTwinWithGeometryVersion(
984
+ twinId: string,
985
+ twinVersion: number,
986
+ signal?: AbortSignal
987
+ ): Promise<T8ClientModel<T8ProjectWithGeometry, T8ProductWithGeometry, T8EntityType, T8Unit> | undefined> {
988
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
989
+
990
+ const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
991
+ const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
992
+
993
+ if (t8cmItem === undefined || t8giItem === undefined) {
994
+ return undefined;
995
+ }
996
+
997
+ const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
998
+ const t8gdItem = twinContent.content.find((item) => item.format === 't8gd');
999
+
1000
+ const downloads = [
1001
+ this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal),
1002
+ ...t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal)),
1003
+ this.downloadTwinContent(twinId, twinVersion, t8giItem, signal),
1004
+ ...(t8gdItem ? [this.downloadTwinContent(twinId, twinVersion, t8gdItem, signal)] : [])
1005
+ ];
1006
+
1007
+ const allData = await Promise.all(downloads);
1008
+
1009
+ const t8cmCount = 1 + t8cmExtensions.length;
1010
+ const t8cmData = allData.slice(0, t8cmCount);
1011
+ const t8giData = allData[t8cmCount];
1012
+ const t8gdData = t8gdItem ? allData[t8cmCount + 1] : undefined;
1013
+
1014
+ return readT8ClientModelWithGeometry(t8cmData, t8giData, t8gdData);
1015
+ }
1016
+
1017
+ async loadTwinWithGeometry(
1018
+ twin: string | { id: string; version: number },
1019
+ signal?: AbortSignal
1020
+ ): Promise<Twin<T8ProjectWithGeometry, T8ProductWithGeometry, T8EntityType, T8Unit>> {
1021
+ let page: string | undefined = undefined;
1022
+ const twinId = typeof twin === 'string' ? twin : twin.id;
1023
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
1024
+
1025
+ if (specifiedVersion !== undefined) {
1026
+ return {
1027
+ id: twinId,
1028
+ version: specifiedVersion,
1029
+ model: await this.loadTwinWithGeometryVersion(twinId, specifiedVersion, signal)
1030
+ };
1031
+ }
1032
+
1033
+ const maxPages = 100;
1034
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
1035
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
1036
+ if (versions.data.length === 0) {
1037
+ break;
1038
+ }
1039
+ for (const versionItem of versions.data) {
1040
+ const model = await this.loadTwinWithGeometryVersion(twinId, versionItem.version, signal);
1041
+ if (model !== undefined) {
1042
+ return {
1043
+ id: twinId,
1044
+ version: versionItem.version,
1045
+ model
1046
+ };
1047
+ }
1048
+ }
1049
+ page = versions.pagination.nextPage;
1050
+ }
1051
+
1052
+ throw new Error(`Twin ${twinId} does not have a client model with geometry`);
1053
+ }
1054
+
1055
+ async createOrUpdateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet> {
1056
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
1057
+ headers: { 'Content-Type': 'application/json' },
1058
+ body: JSON.stringify(twin),
1059
+ signal
1060
+ });
1061
+
1062
+ if (response.ok) {
1063
+ return (await response.json()) as TwinGet;
1064
+ }
1065
+
1066
+ if (response.status === 400) {
1067
+ const errorBody = await readErrorBody(response);
1068
+ throw new TwinValidationError(`Twin validation failed: ${errorBody.detail}`, errorBody);
1069
+ }
1070
+
1071
+ if (response.status === 409) {
1072
+ const conflictedTwin = (await response.json()) as TwinGet;
1073
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
1074
+ }
1075
+
1076
+ const errorText = await response.text();
1077
+ throw new Error(`Failed to create/update twin: ${response.status} ${response.statusText} - ${errorText}`);
1078
+ }
1079
+
1080
+ async updateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet> {
1081
+ return this.createOrUpdateTwin(twinId, twin, signal);
1082
+ }
1083
+
1084
+ async deleteTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet> {
1085
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}`, {
1086
+ headers: { 'Content-Type': 'application/json' },
1087
+ body: JSON.stringify({ etag }),
1088
+ signal
1089
+ });
1090
+
1091
+ if (response.ok) {
1092
+ return (await response.json()) as TwinGet;
1093
+ }
1094
+
1095
+ if (response.status === 404) {
1096
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
1097
+ }
1098
+
1099
+ if (response.status === 409) {
1100
+ const conflictedTwin = (await response.json()) as TwinGet;
1101
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
1102
+ }
1103
+
1104
+ const errorText = await response.text();
1105
+ throw new Error(`Failed to delete twin: ${response.status} ${response.statusText} - ${errorText}`);
1106
+ }
1107
+
1108
+ async restoreTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet> {
1109
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/restore`, {
1110
+ headers: { 'Content-Type': 'application/json' },
1111
+ body: JSON.stringify({ etag }),
1112
+ signal
1113
+ });
1114
+
1115
+ if (response.ok) {
1116
+ return (await response.json()) as TwinGet;
1117
+ }
1118
+
1119
+ if (response.status === 404) {
1120
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
1121
+ }
1122
+
1123
+ if (response.status === 409) {
1124
+ const conflictedTwin = (await response.json()) as TwinGet;
1125
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
1126
+ }
1127
+
1128
+ const errorText = await response.text();
1129
+ throw new Error(`Failed to restore twin: ${response.status} ${response.statusText} - ${errorText}`);
1130
+ }
1131
+
1132
+ async getTwinVersionContent(
1133
+ twinId: string,
1134
+ twinVersion: number,
1135
+ signal?: AbortSignal
1136
+ ): Promise<TwinVersionContentResponse> {
1137
+ const response = await this._httpClient.fetch(
1138
+ HttpMethod.Get,
1139
+ `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`,
1140
+ { headers: { Accept: 'application/json' }, signal }
1141
+ );
1142
+
1143
+ if (response.ok) {
1144
+ return (await response.json()) as TwinVersionContentResponse;
1145
+ }
1146
+
1147
+ if (response.status === 404) {
1148
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
1149
+ }
1150
+
1151
+ const errorText = await response.text();
1152
+ throw new Error(`Failed to get twin version content: ${response.status} ${response.statusText} - ${errorText}`);
1153
+ }
1154
+
1155
+ async getTwinContent(twinId: string, signal?: AbortSignal): Promise<TwinVersionContentResponse> {
1156
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/contents`, {
1157
+ headers: { Accept: 'application/json' },
1158
+ signal
1159
+ });
1160
+
1161
+ if (response.ok) {
1162
+ return (await response.json()) as TwinVersionContentResponse;
1163
+ }
1164
+
1165
+ if (response.status === 404) {
1166
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
1167
+ }
1168
+
1169
+ const errorText = await response.text();
1170
+ throw new Error(`Failed to get twin content: ${response.status} ${response.statusText} - ${errorText}`);
1171
+ }
1172
+
1173
+ async getTwinDiagnostics(twinId: string, signal?: AbortSignal): Promise<TwinDiagnosticsResponse> {
1174
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/diagnostics`, {
1175
+ headers: { Accept: 'application/json' },
1176
+ signal
1177
+ });
1178
+
1179
+ if (response.ok) {
1180
+ return (await response.json()) as TwinDiagnosticsResponse;
1181
+ }
1182
+
1183
+ if (response.status === 404) {
1184
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
1185
+ }
1186
+
1187
+ const errorText = await response.text();
1188
+ throw new Error(`Failed to get twin diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
1189
+ }
1190
+
1191
+ async getTwinVersionDiagnostics(
1192
+ twinId: string,
1193
+ twinVersion: number,
1194
+ signal?: AbortSignal
1195
+ ): Promise<TwinDiagnosticsResponse> {
1196
+ const response = await this._httpClient.fetch(
1197
+ HttpMethod.Get,
1198
+ `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`,
1199
+ { headers: { Accept: 'application/json' }, signal }
1200
+ );
1201
+
1202
+ if (response.ok) {
1203
+ return (await response.json()) as TwinDiagnosticsResponse;
1204
+ }
1205
+
1206
+ if (response.status === 404) {
1207
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
1208
+ }
1209
+
1210
+ const errorText = await response.text();
1211
+ throw new Error(
1212
+ `Failed to get twin version diagnostics: ${response.status} ${response.statusText} - ${errorText}`
1213
+ );
1214
+ }
1215
+
1216
+ async getTwinVersionEvents(
1217
+ twinId: string,
1218
+ twinVersion: number,
1219
+ offset = 0,
1220
+ limit = 100,
1221
+ signal?: AbortSignal
1222
+ ): Promise<PaginatedResponse<TwinProcessorEvent>> {
1223
+ const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/events?offset=${offset}&limit=${limit}`;
1224
+
1225
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1226
+ headers: { Accept: 'application/json' },
1227
+ signal
1228
+ });
1229
+
1230
+ if (response.ok) {
1231
+ return (await response.json()) as PaginatedResponse<TwinProcessorEvent>;
1232
+ }
1233
+
1234
+ if (response.status === 400) {
1235
+ const errorBody = await readErrorBody(response);
1236
+ throw new TwinValidationError(`Twin events validation failed: ${errorBody.detail}`, errorBody);
1237
+ }
1238
+
1239
+ if (response.status === 404) {
1240
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
1241
+ }
1242
+
1243
+ const errorText = await response.text();
1244
+ throw new Error(`Failed to get twin version events: ${response.status} ${response.statusText} - ${errorText}`);
1245
+ }
1246
+
1247
+ async getProcessorEvents(
1248
+ twinId: string,
1249
+ twinVersion: number,
1250
+ processorId: string,
1251
+ offset = 0,
1252
+ limit = 100,
1253
+ signal?: AbortSignal
1254
+ ): Promise<PaginatedResponse<TwinProcessorEvent>> {
1255
+ const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/processors/${processorId}/events?offset=${offset}&limit=${limit}`;
1256
+
1257
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1258
+ headers: { Accept: 'application/json' },
1259
+ signal
1260
+ });
1261
+
1262
+ if (response.ok) {
1263
+ return (await response.json()) as PaginatedResponse<TwinProcessorEvent>;
1264
+ }
1265
+
1266
+ if (response.status === 400) {
1267
+ const errorBody = await readErrorBody(response);
1268
+ throw new TwinValidationError(`Processor events validation failed: ${errorBody.detail}`, errorBody);
1269
+ }
1270
+
1271
+ if (response.status === 404) {
1272
+ throw new ProcessorNotFoundError(
1273
+ `Processor ${processorId} not found for twin ${twinId} version ${twinVersion}`
1274
+ );
1275
+ }
1276
+
1277
+ const errorText = await response.text();
1278
+ throw new Error(`Failed to get processor events: ${response.status} ${response.statusText} - ${errorText}`);
1279
+ }
1280
+
1281
+ async listAssets(
1282
+ twinId: string,
1283
+ offset = 0,
1284
+ limit = 100,
1285
+ options?: { includeDeleted?: boolean },
1286
+ signal?: AbortSignal
1287
+ ): Promise<PaginatedResponse<AssetListItem>> {
1288
+ const includeDeleted = options?.includeDeleted ?? false;
1289
+ const params = `offset=${offset}&limit=${limit}${includeDeleted ? '&includeDeleted=true' : ''}`;
1290
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets?${params}`, {
1291
+ headers: { Accept: 'application/json' },
1292
+ signal
1293
+ });
1294
+
1295
+ if (response.ok) {
1296
+ return (await response.json()) as PaginatedResponse<AssetListItem>;
1297
+ }
1298
+
1299
+ if (response.status === 400) {
1300
+ const errorBody = await readErrorBody(response);
1301
+ throw new AssetValidationError(`Asset listing validation failed: ${errorBody.detail}`, errorBody);
1302
+ }
1303
+
1304
+ if (response.status === 404) {
1305
+ throw new AssetNotFoundError(`Twin ${twinId} not found`);
1306
+ }
1307
+
1308
+ const errorText = await response.text();
1309
+ throw new Error(`Failed to list assets: ${response.status} ${response.statusText} - ${errorText}`);
1310
+ }
1311
+
1312
+ async getAsset(twinId: string, assetId: string, signal?: AbortSignal): Promise<AssetGet> {
1313
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
1314
+ headers: { Accept: 'application/json' },
1315
+ signal
1316
+ });
1317
+
1318
+ if (response.ok) {
1319
+ return (await response.json()) as AssetGet;
1320
+ }
1321
+
1322
+ if (response.status === 404) {
1323
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
1324
+ }
1325
+
1326
+ const errorText = await response.text();
1327
+ throw new Error(`Failed to get asset: ${response.status} ${response.statusText} - ${errorText}`);
1328
+ }
1329
+
1330
+ async createAssets(twinId: string, assets: AssetPost[], signal?: AbortSignal): Promise<AssetGet[]> {
1331
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets`, {
1332
+ headers: { 'Content-Type': 'application/json' },
1333
+ body: JSON.stringify(assets),
1334
+ signal
1335
+ });
1336
+
1337
+ if (response.status === 201) {
1338
+ return (await response.json()) as AssetGet[];
1339
+ }
1340
+
1341
+ if (response.status === 400) {
1342
+ const errorBody = await readErrorBody(response);
1343
+ throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
1344
+ }
1345
+
1346
+ const errorText = await response.text();
1347
+ throw new Error(`Failed to create assets: ${response.status} ${response.statusText} - ${errorText}`);
1348
+ }
1349
+
1350
+ async updateAsset(twinId: string, assetId: string, asset: AssetPut, signal?: AbortSignal): Promise<AssetGet> {
1351
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
1352
+ headers: { 'Content-Type': 'application/json' },
1353
+ body: JSON.stringify(asset),
1354
+ signal
1355
+ });
1356
+
1357
+ if (response.ok) {
1358
+ return (await response.json()) as AssetGet;
1359
+ }
1360
+
1361
+ if (response.status === 409) {
1362
+ const conflictedAsset = (await response.json()) as AssetGet;
1363
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
1364
+ }
1365
+
1366
+ if (response.status === 400) {
1367
+ const errorBody = await readErrorBody(response);
1368
+ throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
1369
+ }
1370
+
1371
+ const errorText = await response.text();
1372
+ throw new Error(`Failed to update asset: ${response.status} ${response.statusText} - ${errorText}`);
1373
+ }
1374
+
1375
+ async deleteAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet> {
1376
+ const response = await this._httpClient.fetch(
1377
+ HttpMethod.Delete,
1378
+ `${this._baseUrl}/${twinId}/assets/${assetId}`,
1379
+ {
1380
+ headers: { 'Content-Type': 'application/json' },
1381
+ body: JSON.stringify({ etag }),
1382
+ signal
1383
+ }
1384
+ );
1385
+
1386
+ if (response.ok) {
1387
+ return (await response.json()) as AssetGet;
1388
+ }
1389
+
1390
+ if (response.status === 409) {
1391
+ const conflictedAsset = (await response.json()) as AssetGet;
1392
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
1393
+ }
1394
+
1395
+ if (response.status === 404) {
1396
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
1397
+ }
1398
+
1399
+ const errorText = await response.text();
1400
+ throw new Error(`Failed to delete asset: ${response.status} ${response.statusText} - ${errorText}`);
1401
+ }
1402
+
1403
+ async restoreAsset(twinId: string, assetId: string, etag: string): Promise<AssetGet> {
1404
+ const response = await this._httpClient.fetch(
1405
+ HttpMethod.Put,
1406
+ `${this._baseUrl}/${twinId}/assets/${assetId}/restore`,
1407
+ {
1408
+ headers: { 'Content-Type': 'application/json' },
1409
+ body: JSON.stringify({ etag })
1410
+ }
1411
+ );
1412
+
1413
+ if (response.ok) {
1414
+ return (await response.json()) as AssetGet;
1415
+ }
1416
+
1417
+ if (response.status === 409) {
1418
+ const conflictedAsset = (await response.json()) as AssetGet;
1419
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
1420
+ }
1421
+
1422
+ if (response.status === 404) {
1423
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
1424
+ }
1425
+
1426
+ const errorText = await response.text();
1427
+ throw new Error(`Failed to restore asset: ${response.status} ${response.statusText} - ${errorText}`);
1428
+ }
1429
+
1430
+ async batchAssets(
1431
+ twinId: string,
1432
+ operations: AssetBatchOperation[],
1433
+ signal?: AbortSignal
1434
+ ): Promise<AssetBatchResponse> {
1435
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch`, {
1436
+ headers: { 'Content-Type': 'application/json' },
1437
+ body: JSON.stringify({ operations }),
1438
+ signal
1439
+ });
1440
+
1441
+ if (response.ok) {
1442
+ return (await response.json()) as AssetBatchResponse;
1443
+ }
1444
+
1445
+ if (response.status === 400) {
1446
+ const errorBody = await readErrorBody(response);
1447
+ throw new AssetValidationError(`Asset batch validation failed: ${errorBody.detail}`, errorBody);
1448
+ }
1449
+
1450
+ if (response.status === 403) {
1451
+ const errorText = await response.text();
1452
+ throw new AssetForbiddenError(`Asset batch forbidden: ${errorText}`);
1453
+ }
1454
+
1455
+ if (response.status === 404 || response.status === 409) {
1456
+ const batchResponse = (await response.json()) as AssetBatchResponse;
1457
+ throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, batchResponse);
1458
+ }
1459
+
1460
+ const errorText = await response.text();
1461
+ throw new Error(`Failed to execute asset batch: ${response.status} ${response.statusText} - ${errorText}`);
1462
+ }
1463
+
1464
+ async batchGetAssets(twinId: string, ids: string[], signal?: AbortSignal): Promise<AssetBatchGetResponse> {
1465
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch-get`, {
1466
+ headers: { 'Content-Type': 'application/json' },
1467
+ body: JSON.stringify({ ids }),
1468
+ signal
1469
+ });
1470
+
1471
+ if (response.ok) {
1472
+ return (await response.json()) as AssetBatchGetResponse;
1473
+ }
1474
+
1475
+ if (response.status === 400) {
1476
+ const errorBody = await readErrorBody(response);
1477
+ throw new AssetValidationError(`Asset batch-get validation failed: ${errorBody.detail}`, errorBody);
1478
+ }
1479
+
1480
+ const errorText = await response.text();
1481
+ throw new Error(`Failed to batch-get assets: ${response.status} ${response.statusText} - ${errorText}`);
1482
+ }
1483
+
1484
+ // ────────── Asset attachments ──────────
1485
+
1486
+ async listAssetAttachments(
1487
+ twinId: string,
1488
+ assetId: string,
1489
+ options?: { includeDeleted?: boolean; offset?: number; limit?: number; page?: string },
1490
+ signal?: AbortSignal
1491
+ ): Promise<PaginatedResponse<AttachmentGet>> {
1492
+ const params = new URLSearchParams();
1493
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
1494
+ if (options?.offset !== undefined) params.set('offset', String(options.offset));
1495
+ if (options?.limit !== undefined) params.set('limit', String(options.limit));
1496
+ if (options?.page) params.set('page', options.page);
1497
+
1498
+ const url = `${this._baseUrl}/${twinId}/assets/${assetId}/attachments${params.toString() ? `?${params}` : ''}`;
1499
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1500
+ headers: { Accept: 'application/json' },
1501
+ signal
1502
+ });
1503
+
1504
+ if (response.ok) return (await response.json()) as PaginatedResponse<AttachmentGet>;
1505
+ if (response.status === 404) throw new AssetNotFoundError(`Asset ${assetId} not found`);
1506
+ if (response.status === 400) {
1507
+ const errorBody = await readErrorBody(response);
1508
+ throw new AssetValidationError(`Attachment listing failed: ${errorBody.detail}`, errorBody);
1509
+ }
1510
+ const errorText = await response.text();
1511
+ throw new Error(`Failed to list attachments: ${response.status} ${response.statusText} - ${errorText}`);
1512
+ }
1513
+
1514
+ async getAssetAttachmentBlob(
1515
+ twinId: string,
1516
+ assetId: string,
1517
+ attachmentId: string,
1518
+ signal?: AbortSignal
1519
+ ): Promise<Response> {
1520
+ const response = await this._httpClient.fetch(
1521
+ HttpMethod.Get,
1522
+ `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`,
1523
+ { signal }
1524
+ );
1525
+ if (response.ok) return response;
1526
+ if (response.status === 404) throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
1527
+ const errorText = await response.text();
1528
+ throw new Error(`Failed to fetch attachment blob: ${response.status} ${response.statusText} - ${errorText}`);
1529
+ }
1530
+
1531
+ async createAssetAttachmentUploadSession(
1532
+ twinId: string,
1533
+ assetId: string,
1534
+ attachmentId: string,
1535
+ body: AttachmentPost,
1536
+ signal?: AbortSignal
1537
+ ): Promise<ApiCreateUploadSessionResponse> {
1538
+ const response = await this._httpClient.fetch(
1539
+ HttpMethod.Post,
1540
+ `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/createAttachmentUploadSession`,
1541
+ {
1542
+ headers: { 'Content-Type': 'application/json' },
1543
+ body: JSON.stringify(body),
1544
+ signal
1545
+ }
1546
+ );
1547
+ if (response.ok) return (await response.json()) as ApiCreateUploadSessionResponse;
1548
+ if (response.status === 409) {
1549
+ const conflicted = (await response.json()) as AttachmentGet;
1550
+ throw new AssetAttachmentConflictError(
1551
+ `Attachment ${attachmentId} already exists; pick a new id for retries`,
1552
+ conflicted
1553
+ );
1554
+ }
1555
+ if (response.status === 404) throw new AssetNotFoundError(`Asset ${assetId} not found`);
1556
+ if (response.status === 400) {
1557
+ const errorBody = await readErrorBody(response);
1558
+ throw new AssetValidationError(`Upload session creation failed: ${errorBody.detail}`, errorBody);
1559
+ }
1560
+ const errorText = await response.text();
1561
+ throw new Error(`Failed to create upload session: ${response.status} ${response.statusText} - ${errorText}`);
1562
+ }
1563
+
1564
+ async uploadAssetAttachmentFile(
1565
+ session: ApiCreateUploadSessionResponse,
1566
+ file: Blob,
1567
+ signal?: AbortSignal
1568
+ ): Promise<{ token: string }> {
1569
+ // The token is encoded as a query param on the upload URL; we lift it
1570
+ // out for the caller so they can poll uploadStatus without re-parsing.
1571
+ const tokenMatch = session.uploadUrl.match(/token=([^&]+)/);
1572
+ if (!tokenMatch) {
1573
+ throw new Error(`Upload session URL is missing a token query parameter: ${session.uploadUrl}`);
1574
+ }
1575
+ const token = tokenMatch[1];
1576
+
1577
+ const chunkSize = Math.max(1, session.maxChunkSize || file.size);
1578
+ const total = file.size;
1579
+ let offset = 0;
1580
+
1581
+ // Even an empty file needs at least one PUT so the upload session
1582
+ // gets a finalize signal. uploadChunks() in the integration tests
1583
+ // unconditionally PUTs at least one chunk too.
1584
+ do {
1585
+ const end = Math.min(offset + chunkSize, total);
1586
+ const chunk = file.slice(offset, end);
1587
+ const lastByte = Math.max(0, end - 1);
1588
+ const response = await this._httpClient.fetch(HttpMethod.Put, session.uploadUrl, {
1589
+ headers: { 'Content-Range': `bytes ${offset}-${lastByte}/${total}` },
1590
+ body: chunk,
1591
+ signal
1592
+ });
1593
+ if (!response.ok) {
1594
+ const errorText = await response.text();
1595
+ throw new Error(
1596
+ `Upload chunk failed at offset ${offset}: ${response.status} ${response.statusText} - ${errorText}`
1597
+ );
1598
+ }
1599
+ offset = end;
1600
+ } while (offset < total);
1601
+
1602
+ return { token };
1603
+ }
1604
+
1605
+ async getAssetAttachmentUploadStatus(
1606
+ twinId: string,
1607
+ assetId: string,
1608
+ token: string,
1609
+ signal?: AbortSignal
1610
+ ): Promise<UploadStatusGet> {
1611
+ const response = await this._httpClient.fetch(
1612
+ HttpMethod.Get,
1613
+ `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/uploadStatus/${encodeURIComponent(token)}`,
1614
+ { headers: { Accept: 'application/json' }, signal }
1615
+ );
1616
+ if (response.ok) return (await response.json()) as UploadStatusGet;
1617
+ if (response.status === 404) return { status: 'NotFound' };
1618
+ const errorText = await response.text();
1619
+ throw new Error(`Failed to get upload status: ${response.status} ${response.statusText} - ${errorText}`);
1620
+ }
1621
+
1622
+ async putAssetAttachment(
1623
+ twinId: string,
1624
+ assetId: string,
1625
+ attachmentId: string,
1626
+ body: AssetAttachmentPut,
1627
+ signal?: AbortSignal
1628
+ ): Promise<AttachmentGet> {
1629
+ const response = await this._httpClient.fetch(
1630
+ HttpMethod.Put,
1631
+ `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`,
1632
+ {
1633
+ headers: { 'Content-Type': 'application/json' },
1634
+ body: JSON.stringify(body),
1635
+ signal
1636
+ }
1637
+ );
1638
+ if (response.ok) return (await response.json()) as AttachmentGet;
1639
+ if (response.status === 409) {
1640
+ const conflicted = (await response.json()) as AttachmentGet;
1641
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
1642
+ }
1643
+ if (response.status === 404) throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
1644
+ if (response.status === 400) {
1645
+ const errorBody = await readErrorBody(response);
1646
+ throw new AssetValidationError(`Attachment update failed: ${errorBody.detail}`, errorBody);
1647
+ }
1648
+ const errorText = await response.text();
1649
+ throw new Error(`Failed to update attachment: ${response.status} ${response.statusText} - ${errorText}`);
1650
+ }
1651
+
1652
+ async deleteAssetAttachment(
1653
+ twinId: string,
1654
+ assetId: string,
1655
+ attachmentId: string,
1656
+ etag: string,
1657
+ signal?: AbortSignal
1658
+ ): Promise<AttachmentGet> {
1659
+ const response = await this._httpClient.fetch(
1660
+ HttpMethod.Delete,
1661
+ `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`,
1662
+ {
1663
+ headers: { 'If-Match': etag },
1664
+ signal
1665
+ }
1666
+ );
1667
+ if (response.ok) return (await response.json()) as AttachmentGet;
1668
+ if (response.status === 409) {
1669
+ const conflicted = (await response.json()) as AttachmentGet;
1670
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
1671
+ }
1672
+ if (response.status === 404) throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
1673
+ const errorText = await response.text();
1674
+ throw new Error(`Failed to delete attachment: ${response.status} ${response.statusText} - ${errorText}`);
1675
+ }
1676
+
1677
+ async restoreAssetAttachment(
1678
+ twinId: string,
1679
+ assetId: string,
1680
+ attachmentId: string,
1681
+ etag: string,
1682
+ signal?: AbortSignal
1683
+ ): Promise<AttachmentGet> {
1684
+ const response = await this._httpClient.fetch(
1685
+ HttpMethod.Put,
1686
+ `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/restore`,
1687
+ {
1688
+ headers: { 'Content-Type': 'application/json' },
1689
+ body: JSON.stringify({ etag }),
1690
+ signal
1691
+ }
1692
+ );
1693
+ if (response.ok) return (await response.json()) as AttachmentGet;
1694
+ if (response.status === 409) {
1695
+ const conflicted = (await response.json()) as AttachmentGet;
1696
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
1697
+ }
1698
+ if (response.status === 404) throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
1699
+ const errorText = await response.text();
1700
+ throw new Error(`Failed to restore attachment: ${response.status} ${response.statusText} - ${errorText}`);
1701
+ }
1702
+
1703
+ async listTwinFiles(
1704
+ twinId: string,
1705
+ options: TwinFileListOptions = {},
1706
+ signal?: AbortSignal
1707
+ ): Promise<PaginatedResponse<TwinFileGet>> {
1708
+ const { includeDeleted = false, checkedIn = false, version, page = undefined, limit = 100 } = options;
1709
+
1710
+ if (version !== undefined && !checkedIn) {
1711
+ throw new Error('The "version" option can only be used when "checkedIn" is true.');
1712
+ }
1713
+
1714
+ const params = new URLSearchParams();
1715
+ if (includeDeleted) params.set('includeDeleted', 'true');
1716
+ if (checkedIn) params.set('checkedIn', 'true');
1717
+ if (version !== undefined) params.set('version', version.toString());
1718
+ if (page !== undefined) params.set('page', page);
1719
+ if (limit !== 100) params.set('limit', limit.toString());
1720
+
1721
+ const url = params.toString()
1722
+ ? `${this._baseUrl}/${twinId}/files?${params}`
1723
+ : `${this._baseUrl}/${twinId}/files`;
1724
+
1725
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1726
+ headers: { Accept: 'application/json' },
1727
+ signal
1728
+ });
1729
+
1730
+ if (response.ok) {
1731
+ return (await response.json()) as PaginatedResponse<TwinFileGet>;
1732
+ }
1733
+
1734
+ if (response.status === 400) {
1735
+ const errorBody = await readErrorBody(response);
1736
+ throw new TwinFileValidationError(`Twin file listing validation failed: ${errorBody.detail}`, errorBody);
1737
+ }
1738
+
1739
+ const errorText = await response.text();
1740
+ throw new Error(`Failed to list twin files: ${response.status} ${response.statusText} - ${errorText}`);
1741
+ }
1742
+
1743
+ async getTwinFile(twinId: string, fileId: string, signal?: AbortSignal): Promise<TwinFileGet> {
1744
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1745
+ headers: { Accept: 'application/json' },
1746
+ signal
1747
+ });
1748
+
1749
+ if (response.ok) {
1750
+ return (await response.json()) as TwinFileGet;
1751
+ }
1752
+
1753
+ if (response.status === 404) {
1754
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1755
+ }
1756
+
1757
+ if (response.status === 400) {
1758
+ const errorText = await response.text();
1759
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1760
+ }
1761
+
1762
+ const errorText = await response.text();
1763
+ throw new Error(`Failed to get twin file: ${response.status} ${response.statusText} - ${errorText}`);
1764
+ }
1765
+
1766
+ async createOrUpdateTwinFile(
1767
+ twinId: string,
1768
+ fileId: string,
1769
+ twinFile: TwinFilePut,
1770
+ signal?: AbortSignal
1771
+ ): Promise<TwinFileGet> {
1772
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1773
+ headers: { 'Content-Type': 'application/json' },
1774
+ body: JSON.stringify(twinFile),
1775
+ signal
1776
+ });
1777
+
1778
+ if (response.ok || response.status === 201) {
1779
+ return (await response.json()) as TwinFileGet;
1780
+ }
1781
+
1782
+ if (response.status === 400) {
1783
+ const errorBody = await readErrorBody(response);
1784
+ throw new TwinFileValidationError(`Twin file validation failed`, errorBody);
1785
+ }
1786
+
1787
+ if (response.status === 409) {
1788
+ const conflictedFile = (await response.json()) as TwinFileGet;
1789
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1790
+ }
1791
+
1792
+ const errorText = await response.text();
1793
+ throw new Error(`Failed to create/update twin file: ${response.status} ${response.statusText} - ${errorText}`);
1794
+ }
1795
+
1796
+ async deleteTwinFile(
1797
+ twinId: string,
1798
+ fileId: string,
1799
+ deleteRequest: TwinFileDelete,
1800
+ signal?: AbortSignal
1801
+ ): Promise<TwinFileGet> {
1802
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1803
+ headers: { 'Content-Type': 'application/json' },
1804
+ body: JSON.stringify(deleteRequest),
1805
+ signal
1806
+ });
1807
+
1808
+ if (response.ok) {
1809
+ return (await response.json()) as TwinFileGet;
1810
+ }
1811
+
1812
+ if (response.status === 404) {
1813
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1814
+ }
1815
+
1816
+ if (response.status === 409) {
1817
+ const conflictedFile = (await response.json()) as TwinFileGet;
1818
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1819
+ }
1820
+
1821
+ if (response.status === 400) {
1822
+ const errorText = await response.text();
1823
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1824
+ }
1825
+
1826
+ const errorText = await response.text();
1827
+ throw new Error(`Failed to delete twin file: ${response.status} ${response.statusText} - ${errorText}`);
1828
+ }
1829
+
1830
+ async restoreTwinFile(twinId: string, fileId: string, etag: string, signal?: AbortSignal): Promise<TwinFileGet> {
1831
+ const response = await this._httpClient.fetch(
1832
+ HttpMethod.Put,
1833
+ `${this._baseUrl}/${twinId}/files/${fileId}/restore`,
1834
+ {
1835
+ headers: { 'Content-Type': 'application/json' },
1836
+ body: JSON.stringify({ etag }),
1837
+ signal
1838
+ }
1839
+ );
1840
+
1841
+ if (response.ok) {
1842
+ return (await response.json()) as TwinFileGet;
1843
+ }
1844
+
1845
+ if (response.status === 404) {
1846
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1847
+ }
1848
+
1849
+ if (response.status === 409) {
1850
+ const conflictedFile = (await response.json()) as TwinFileGet;
1851
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1852
+ }
1853
+
1854
+ if (response.status === 400) {
1855
+ const errorText = await response.text();
1856
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1857
+ }
1858
+
1859
+ const errorText = await response.text();
1860
+ throw new Error(`Failed to restore twin file: ${response.status} ${response.statusText} - ${errorText}`);
1861
+ }
1862
+
1863
+ async getTwinFileContent(twinId: string, fileId: string, signal?: AbortSignal): Promise<Blob> {
1864
+ const response = await this._httpClient.fetch(
1865
+ HttpMethod.Get,
1866
+ `${this._baseUrl}/${twinId}/files/${fileId}/content`,
1867
+ { signal }
1868
+ );
1869
+
1870
+ if (response.ok) {
1871
+ return await response.blob();
1872
+ }
1873
+
1874
+ if (response.status === 404) {
1875
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1876
+ }
1877
+
1878
+ const errorText = await response.text();
1879
+ throw new Error(`Failed to get twin file content: ${response.status} ${response.statusText} - ${errorText}`);
1880
+ }
1881
+
1882
+ async getTwinFileVersion(
1883
+ twinId: string,
1884
+ fileId: string,
1885
+ version: number,
1886
+ signal?: AbortSignal
1887
+ ): Promise<TwinFileGet> {
1888
+ const response = await this._httpClient.fetch(
1889
+ HttpMethod.Get,
1890
+ `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}`,
1891
+ { signal }
1892
+ );
1893
+
1894
+ if (response.ok) {
1895
+ return (await response.json()) as TwinFileGet;
1896
+ }
1897
+
1898
+ if (response.status === 404) {
1899
+ throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} not found`);
1900
+ }
1901
+
1902
+ const errorText = await response.text();
1903
+ throw new Error(`Failed to get twin file version: ${response.status} ${response.statusText} - ${errorText}`);
1904
+ }
1905
+
1906
+ async listTwinFileVersions(
1907
+ twinId: string,
1908
+ fileId: string,
1909
+ options?: TwinFileVersionListOptions,
1910
+ signal?: AbortSignal
1911
+ ): Promise<PaginatedResponse<TwinFileGet>> {
1912
+ const params = new URLSearchParams();
1913
+ if (options?.includeDeleted !== undefined) params.append('includeDeleted', String(options.includeDeleted));
1914
+ if (options?.offset !== undefined) params.append('offset', String(options.offset));
1915
+ if (options?.limit !== undefined) params.append('limit', String(options.limit));
1916
+
1917
+ const response = await this._httpClient.fetch(
1918
+ HttpMethod.Get,
1919
+ `${this._baseUrl}/${twinId}/files/${fileId}/versions?${params.toString()}`,
1920
+ { signal }
1921
+ );
1922
+
1923
+ if (response.ok) {
1924
+ return (await response.json()) as PaginatedResponse<TwinFileGet>;
1925
+ }
1926
+
1927
+ if (response.status === 400) {
1928
+ const errorBody = await readErrorBody(response);
1929
+ throw new TwinFileValidationError(
1930
+ `Twin file version listing validation failed: ${errorBody.detail}`,
1931
+ errorBody
1932
+ );
1933
+ }
1934
+
1935
+ const errorText = await response.text();
1936
+ throw new Error(`Failed to list twin file versions: ${response.status} ${response.statusText} - ${errorText}`);
1937
+ }
1938
+
1939
+ async getTwinFileVersionContent(
1940
+ twinId: string,
1941
+ fileId: string,
1942
+ version: number,
1943
+ signal?: AbortSignal
1944
+ ): Promise<Blob> {
1945
+ const response = await this._httpClient.fetch(
1946
+ HttpMethod.Get,
1947
+ `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}/content`,
1948
+ { signal }
1949
+ );
1950
+
1951
+ if (response.ok) {
1952
+ return await response.blob();
1953
+ }
1954
+
1955
+ if (response.status === 404) {
1956
+ throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} content not found`);
1957
+ }
1958
+
1959
+ const errorText = await response.text();
1960
+ throw new Error(
1961
+ `Failed to get twin file version content: ${response.status} ${response.statusText} - ${errorText}`
1962
+ );
1963
+ }
1964
+
1965
+ async createOrUpdateTwinRaw(twinId: string, twin: unknown, signal?: AbortSignal): Promise<Response> {
1966
+ return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
1967
+ headers: { 'Content-Type': 'application/json' },
1968
+ body: JSON.stringify(twin),
1969
+ signal
1970
+ });
1971
+ }
1972
+
1973
+ async createOrUpdateTwinFileRaw(
1974
+ twinId: string,
1975
+ fileId: string,
1976
+ twinFile: unknown,
1977
+ signal?: AbortSignal
1978
+ ): Promise<Response> {
1979
+ return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1980
+ headers: { 'Content-Type': 'application/json' },
1981
+ body: JSON.stringify(twinFile),
1982
+ signal
1983
+ });
1984
+ }
1985
+
1986
+ // ---------- Asset categories ----------
1987
+
1988
+ async getMyPermissions(signal?: AbortSignal): Promise<MyPermissions> {
1989
+ const url = `${this._baseUrl}/permissions/me`;
1990
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1991
+ headers: { Accept: 'application/json' },
1992
+ signal
1993
+ });
1994
+ if (response.ok) {
1995
+ return (await response.json()) as MyPermissions;
1996
+ }
1997
+ // 401: no valid auth context. Throw so consumers can redirect to
1998
+ // login / refresh tokens rather than mistake an expired session for
1999
+ // "logged-in user with no grants."
2000
+ if (response.status === 401) {
2001
+ throw new TwinUnauthenticatedError('Not authenticated');
2002
+ }
2003
+ // 403: authenticated but no Twin-service capabilities. Surface as an
2004
+ // empty array so consumers can treat it like a logged-in user with
2005
+ // zero grants.
2006
+ if (response.status === 403) {
2007
+ return { permissions: [] };
2008
+ }
2009
+ throw new Error(`Failed to fetch permissions: ${response.status} ${response.statusText}`);
2010
+ }
2011
+
2012
+ async getAssetTypeMetaSchema(signal?: AbortSignal): Promise<Record<string, unknown>> {
2013
+ const url = `${this._assetTypeCatalogUrl}/meta-schema`;
2014
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
2015
+ headers: { Accept: 'application/schema+json, application/json' },
2016
+ signal
2017
+ });
2018
+ if (!response.ok) {
2019
+ throw new Error(`Failed to fetch meta-schema: ${response.status} ${response.statusText}`);
2020
+ }
2021
+ return (await response.json()) as Record<string, unknown>;
2022
+ }
2023
+
2024
+ async listAssetCategories(
2025
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
2026
+ signal?: AbortSignal
2027
+ ): Promise<PaginatedResponse<AssetCategoryGet>> {
2028
+ const params = new URLSearchParams();
2029
+ if (options?.page !== undefined) params.set('page', options.page);
2030
+ if (options?.limit !== undefined) params.set('limit', options.limit.toString());
2031
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
2032
+
2033
+ const url = params.toString()
2034
+ ? `${this._assetTypeCatalogUrl}/categories?${params}`
2035
+ : `${this._assetTypeCatalogUrl}/categories`;
2036
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
2037
+ headers: { Accept: 'application/json' },
2038
+ signal
2039
+ });
2040
+
2041
+ if (response.ok) return (await response.json()) as PaginatedResponse<AssetCategoryGet>;
2042
+ if (response.status === 403) throw new AssetCategoryForbiddenError('Asset category listing forbidden');
2043
+ if (response.status === 400) {
2044
+ const errorBody = await readErrorBody(response);
2045
+ throw new AssetCategoryValidationError(`Asset category listing failed: ${errorBody.detail}`, errorBody);
2046
+ }
2047
+ throw new Error(`Failed to list asset categories: ${response.status} ${response.statusText}`);
2048
+ }
2049
+
2050
+ async getAssetCategory(categoryId: string, signal?: AbortSignal): Promise<AssetCategoryGet> {
2051
+ const response = await this._httpClient.fetch(
2052
+ HttpMethod.Get,
2053
+ `${this._assetTypeCatalogUrl}/categories/${categoryId}`,
2054
+ {
2055
+ headers: { Accept: 'application/json' },
2056
+ signal
2057
+ }
2058
+ );
2059
+ if (response.ok) return (await response.json()) as AssetCategoryGet;
2060
+ if (response.status === 403)
2061
+ throw new AssetCategoryForbiddenError(`Asset category ${categoryId} read forbidden`);
2062
+ if (response.status === 404) throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
2063
+ throw new Error(`Failed to get asset category: ${response.status} ${response.statusText}`);
2064
+ }
2065
+
2066
+ async putAssetCategory(
2067
+ categoryId: string,
2068
+ body: AssetCategoryPut,
2069
+ signal?: AbortSignal
2070
+ ): Promise<AssetCategoryGet> {
2071
+ const response = await this._httpClient.fetch(
2072
+ HttpMethod.Put,
2073
+ `${this._assetTypeCatalogUrl}/categories/${categoryId}`,
2074
+ {
2075
+ headers: { 'Content-Type': 'application/json' },
2076
+ body: JSON.stringify(body),
2077
+ signal
2078
+ }
2079
+ );
2080
+ if (response.ok) return (await response.json()) as AssetCategoryGet;
2081
+ if (response.status === 403) throw new AssetCategoryForbiddenError('Asset category upsert forbidden');
2082
+ if (response.status === 409) {
2083
+ const current = (await response.json()) as AssetCategoryGet;
2084
+ throw new AssetCategoryConflictError('Asset category was modified by another user', current);
2085
+ }
2086
+ if (response.status === 400) {
2087
+ const errorBody = await readErrorBody(response);
2088
+ throw new AssetCategoryValidationError(`Asset category validation failed: ${errorBody.detail}`, errorBody);
2089
+ }
2090
+ const errorText = await response.text();
2091
+ throw new Error(`Failed to upsert asset category: ${response.status} ${response.statusText} - ${errorText}`);
2092
+ }
2093
+
2094
+ async deleteAssetCategory(categoryId: string, etag: string, signal?: AbortSignal): Promise<AssetCategoryGet> {
2095
+ return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ false, signal);
2096
+ }
2097
+
2098
+ async restoreAssetCategory(categoryId: string, etag: string, signal?: AbortSignal): Promise<AssetCategoryGet> {
2099
+ return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ true, signal);
2100
+ }
2101
+
2102
+ private async _categoryDeleteOrRestore(
2103
+ categoryId: string,
2104
+ etag: string,
2105
+ isRestore: boolean,
2106
+ signal?: AbortSignal
2107
+ ): Promise<AssetCategoryGet> {
2108
+ const url = isRestore
2109
+ ? `${this._assetTypeCatalogUrl}/categories/${categoryId}/restore`
2110
+ : `${this._assetTypeCatalogUrl}/categories/${categoryId}`;
2111
+ const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
2112
+ const response = await this._httpClient.fetch(method, url, {
2113
+ headers: { 'Content-Type': 'application/json' },
2114
+ body: JSON.stringify({ etag }),
2115
+ signal
2116
+ });
2117
+ if (response.ok) return (await response.json()) as AssetCategoryGet;
2118
+ if (response.status === 403) {
2119
+ const op = isRestore ? 'restore' : 'delete';
2120
+ throw new AssetCategoryForbiddenError(`Asset category ${op} forbidden`);
2121
+ }
2122
+ if (response.status === 409) {
2123
+ const current = (await response.json()) as AssetCategoryGet;
2124
+ throw new AssetCategoryConflictError('Asset category was modified by another user', current);
2125
+ }
2126
+ if (response.status === 404) throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
2127
+ const errorText = await response.text();
2128
+ const op = isRestore ? 'restore' : 'delete';
2129
+ throw new Error(`Failed to ${op} asset category: ${response.status} ${response.statusText} - ${errorText}`);
2130
+ }
2131
+
2132
+ async listAssetCategoryVersions(
2133
+ categoryId: string,
2134
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
2135
+ signal?: AbortSignal
2136
+ ): Promise<PaginatedResponse<AssetCategoryGet>> {
2137
+ const params = new URLSearchParams();
2138
+ if (options?.page !== undefined) params.set('page', options.page);
2139
+ if (options?.limit !== undefined) params.set('limit', options.limit.toString());
2140
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
2141
+
2142
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/versions`;
2143
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
2144
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
2145
+ headers: { Accept: 'application/json' },
2146
+ signal
2147
+ });
2148
+
2149
+ if (response.ok) return (await response.json()) as PaginatedResponse<AssetCategoryGet>;
2150
+ if (response.status === 403) throw new AssetCategoryForbiddenError('Asset category version listing forbidden');
2151
+ if (response.status === 400) {
2152
+ const errorBody = await readErrorBody(response);
2153
+ throw new AssetCategoryValidationError(
2154
+ `Asset category version listing failed: ${errorBody.detail}`,
2155
+ errorBody
2156
+ );
2157
+ }
2158
+ if (response.status === 404) throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
2159
+ throw new Error(`Failed to list asset category versions: ${response.status} ${response.statusText}`);
2160
+ }
2161
+
2162
+ // ---------- Asset types ----------
2163
+
2164
+ async listAssetTypes(
2165
+ categoryId: string,
2166
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
2167
+ signal?: AbortSignal
2168
+ ): Promise<PaginatedResponse<AssetTypeGet>> {
2169
+ const params = new URLSearchParams();
2170
+ if (options?.page !== undefined) params.set('page', options.page);
2171
+ if (options?.limit !== undefined) params.set('limit', options.limit.toString());
2172
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
2173
+
2174
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types`;
2175
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
2176
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
2177
+ headers: { Accept: 'application/json' },
2178
+ signal
2179
+ });
2180
+
2181
+ if (response.ok) return (await response.json()) as PaginatedResponse<AssetTypeGet>;
2182
+ if (response.status === 403) throw new AssetTypeForbiddenError('Asset type listing forbidden');
2183
+ if (response.status === 400) {
2184
+ const errorBody = await readErrorBody(response);
2185
+ throw new AssetTypeValidationError(`Asset type listing failed: ${errorBody.detail}`, errorBody);
2186
+ }
2187
+ throw new Error(`Failed to list asset types: ${response.status} ${response.statusText}`);
2188
+ }
2189
+
2190
+ async getAssetType(categoryId: string, typeId: string, signal?: AbortSignal): Promise<AssetTypeGet> {
2191
+ const response = await this._httpClient.fetch(
2192
+ HttpMethod.Get,
2193
+ `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`,
2194
+ { headers: { Accept: 'application/json' }, signal }
2195
+ );
2196
+ if (response.ok) return (await response.json()) as AssetTypeGet;
2197
+ if (response.status === 403) throw new AssetTypeForbiddenError(`Asset type ${typeId} read forbidden`);
2198
+ if (response.status === 404) throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
2199
+ throw new Error(`Failed to get asset type: ${response.status} ${response.statusText}`);
2200
+ }
2201
+
2202
+ async listAssetTypeVersions(
2203
+ categoryId: string,
2204
+ typeId: string,
2205
+ options?: { page?: string; limit?: number; includeDeleted?: boolean },
2206
+ signal?: AbortSignal
2207
+ ): Promise<PaginatedResponse<AssetTypeGet>> {
2208
+ const params = new URLSearchParams();
2209
+ if (options?.page !== undefined) params.set('page', options.page);
2210
+ if (options?.limit !== undefined) params.set('limit', options.limit.toString());
2211
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
2212
+
2213
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/versions`;
2214
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
2215
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
2216
+ headers: { Accept: 'application/json' },
2217
+ signal
2218
+ });
2219
+
2220
+ if (response.ok) return (await response.json()) as PaginatedResponse<AssetTypeGet>;
2221
+ if (response.status === 403) throw new AssetTypeForbiddenError('Asset type version listing forbidden');
2222
+ if (response.status === 400) {
2223
+ const errorBody = await readErrorBody(response);
2224
+ throw new AssetTypeValidationError(`Asset type version listing failed: ${errorBody.detail}`, errorBody);
2225
+ }
2226
+ if (response.status === 404) throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
2227
+ throw new Error(`Failed to list asset type versions: ${response.status} ${response.statusText}`);
2228
+ }
2229
+
2230
+ async putAssetType(
2231
+ categoryId: string,
2232
+ typeId: string,
2233
+ body: AssetTypePut,
2234
+ signal?: AbortSignal
2235
+ ): Promise<AssetTypeGet> {
2236
+ const response = await this._httpClient.fetch(
2237
+ HttpMethod.Put,
2238
+ `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`,
2239
+ {
2240
+ headers: { 'Content-Type': 'application/json' },
2241
+ body: JSON.stringify(body),
2242
+ signal
2243
+ }
2244
+ );
2245
+ if (response.ok) return (await response.json()) as AssetTypeGet;
2246
+ if (response.status === 403) throw new AssetTypeForbiddenError('Asset type upsert forbidden');
2247
+ if (response.status === 409) {
2248
+ const current = (await response.json()) as AssetTypeGet;
2249
+ throw new AssetTypeConflictError('Asset type was modified by another user', current);
2250
+ }
2251
+ if (response.status === 400) {
2252
+ const errorBody = await readErrorBody(response);
2253
+ throw new AssetTypeValidationError(`Asset type validation failed: ${errorBody.detail}`, errorBody);
2254
+ }
2255
+ const errorText = await response.text();
2256
+ throw new Error(`Failed to upsert asset type: ${response.status} ${response.statusText} - ${errorText}`);
2257
+ }
2258
+
2259
+ async deleteAssetType(
2260
+ categoryId: string,
2261
+ typeId: string,
2262
+ etag: string,
2263
+ signal?: AbortSignal
2264
+ ): Promise<AssetTypeGet> {
2265
+ return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ false, signal);
2266
+ }
2267
+
2268
+ async restoreAssetType(
2269
+ categoryId: string,
2270
+ typeId: string,
2271
+ etag: string,
2272
+ signal?: AbortSignal
2273
+ ): Promise<AssetTypeGet> {
2274
+ return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ true, signal);
2275
+ }
2276
+
2277
+ private async _typeDeleteOrRestore(
2278
+ categoryId: string,
2279
+ typeId: string,
2280
+ etag: string,
2281
+ isRestore: boolean,
2282
+ signal?: AbortSignal
2283
+ ): Promise<AssetTypeGet> {
2284
+ const url = isRestore
2285
+ ? `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/restore`
2286
+ : `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`;
2287
+ const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
2288
+ const response = await this._httpClient.fetch(method, url, {
2289
+ headers: { 'Content-Type': 'application/json' },
2290
+ body: JSON.stringify({ etag }),
2291
+ signal
2292
+ });
2293
+ if (response.ok) return (await response.json()) as AssetTypeGet;
2294
+ if (response.status === 403) {
2295
+ const op = isRestore ? 'restore' : 'delete';
2296
+ throw new AssetTypeForbiddenError(`Asset type ${op} forbidden`);
2297
+ }
2298
+ if (response.status === 409) {
2299
+ const current = (await response.json()) as AssetTypeGet;
2300
+ throw new AssetTypeConflictError('Asset type was modified by another user', current);
2301
+ }
2302
+ if (response.status === 404) throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
2303
+ const errorText = await response.text();
2304
+ const op = isRestore ? 'restore' : 'delete';
2305
+ throw new Error(`Failed to ${op} asset type: ${response.status} ${response.statusText} - ${errorText}`);
2306
+ }
2307
+
2308
+ async getAssetTypeCatalog(signal?: AbortSignal): Promise<AssetCatalogResponse> {
2309
+ const response = await this._httpClient.fetch(HttpMethod.Get, this._assetTypeCatalogUrl, {
2310
+ headers: { Accept: 'application/json' },
2311
+ signal
2312
+ });
2313
+ if (response.ok) return (await response.json()) as AssetCatalogResponse;
2314
+ if (response.status === 403) throw new AssetCategoryForbiddenError('Asset type catalog read forbidden');
2315
+ if (response.status === 400) {
2316
+ // The server emits a single structured 400 here today —
2317
+ // `cause: 'CatalogTooLarge'` — to flag a tenant whose catalog
2318
+ // overflows the per-customer cap. Surface as a typed error so
2319
+ // picker UIs can render "use search" instead of crashing on
2320
+ // missing rows. Any other 400 (malformed CatalogTooLarge body,
2321
+ // future cause) falls through to the generic error below so the
2322
+ // caller still sees the raw response.
2323
+ const errorBody = await readErrorBody(response);
2324
+ if (isAssetCatalogTooLargeErrorBody(errorBody)) {
2325
+ throw new AssetCatalogTooLargeError(`Asset type catalog too large: ${errorBody.detail}`, errorBody);
2326
+ }
2327
+ throw new Error(
2328
+ `Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorBody.detail}`
2329
+ );
2330
+ }
2331
+ const errorText = await response.text();
2332
+ throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorText}`);
2333
+ }
2334
+ }