@twinfinity/twin 5.0.3-ci.27942

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 (168) hide show
  1. package/dist/BuildInfo.d.ts +25 -0
  2. package/dist/BuildInfo.d.ts.map +1 -0
  3. package/dist/BuildInfo.js +15 -0
  4. package/dist/BuildInfo.js.map +1 -0
  5. package/dist/TwinClient.d.ts +156 -0
  6. package/dist/TwinClient.d.ts.map +1 -0
  7. package/dist/TwinClient.js +682 -0
  8. package/dist/TwinClient.js.map +1 -0
  9. package/dist/errors.d.ts +51 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +79 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +4 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/tsconfig.tsbuildinfo +1 -0
  18. package/dist/types/Twin.d.ts +7 -0
  19. package/dist/types/Twin.d.ts.map +1 -0
  20. package/dist/types/Twin.js +6 -0
  21. package/dist/types/Twin.js.map +1 -0
  22. package/dist/types/TwinGeometryInfo.d.ts +7 -0
  23. package/dist/types/TwinGeometryInfo.d.ts.map +1 -0
  24. package/dist/types/TwinGeometryInfo.js +2 -0
  25. package/dist/types/TwinGeometryInfo.js.map +1 -0
  26. package/dist/types/asset-api.d.ts +57 -0
  27. package/dist/types/asset-api.d.ts.map +1 -0
  28. package/dist/types/asset-api.js +2 -0
  29. package/dist/types/asset-api.js.map +1 -0
  30. package/dist/types/asset-batch-api.d.ts +71 -0
  31. package/dist/types/asset-batch-api.d.ts.map +1 -0
  32. package/dist/types/asset-batch-api.js +2 -0
  33. package/dist/types/asset-batch-api.js.map +1 -0
  34. package/dist/types/common.d.ts +10 -0
  35. package/dist/types/common.d.ts.map +1 -0
  36. package/dist/types/common.js +2 -0
  37. package/dist/types/common.js.map +1 -0
  38. package/dist/types/generated.d.ts +388 -0
  39. package/dist/types/generated.d.ts.map +1 -0
  40. package/dist/types/generated.js +2 -0
  41. package/dist/types/generated.js.map +1 -0
  42. package/dist/types/index.d.ts +7 -0
  43. package/dist/types/index.d.ts.map +1 -0
  44. package/dist/types/index.js +7 -0
  45. package/dist/types/index.js.map +1 -0
  46. package/dist/types/twin-api.d.ts +88 -0
  47. package/dist/types/twin-api.d.ts.map +1 -0
  48. package/dist/types/twin-api.js +2 -0
  49. package/dist/types/twin-api.js.map +1 -0
  50. package/dist/types/twin-file-api.d.ts +61 -0
  51. package/dist/types/twin-file-api.d.ts.map +1 -0
  52. package/dist/types/twin-file-api.js +2 -0
  53. package/dist/types/twin-file-api.js.map +1 -0
  54. package/docs/.nojekyll +1 -0
  55. package/docs/assets/hierarchy.js +1 -0
  56. package/docs/assets/highlight.css +22 -0
  57. package/docs/assets/icons.js +18 -0
  58. package/docs/assets/icons.svg +1 -0
  59. package/docs/assets/main.js +60 -0
  60. package/docs/assets/navigation.js +1 -0
  61. package/docs/assets/search.js +1 -0
  62. package/docs/assets/style.css +1633 -0
  63. package/docs/classes/AssetBatchError.html +3 -0
  64. package/docs/classes/AssetConflictError.html +3 -0
  65. package/docs/classes/AssetNotFoundError.html +2 -0
  66. package/docs/classes/AssetValidationError.html +2 -0
  67. package/docs/classes/ProcessorNotFoundError.html +2 -0
  68. package/docs/classes/TwinClient.html +59 -0
  69. package/docs/classes/TwinConflictError.html +3 -0
  70. package/docs/classes/TwinFileConflictError.html +3 -0
  71. package/docs/classes/TwinFileNotFoundError.html +2 -0
  72. package/docs/classes/TwinFileValidationError.html +3 -0
  73. package/docs/classes/TwinNotFoundError.html +2 -0
  74. package/docs/classes/TwinValidationError.html +3 -0
  75. package/docs/hierarchy.html +1 -0
  76. package/docs/index.html +1 -0
  77. package/docs/interfaces/AssetBatchDeleteOperation.html +4 -0
  78. package/docs/interfaces/AssetBatchGetRequest.html +2 -0
  79. package/docs/interfaces/AssetBatchGetResponse.html +3 -0
  80. package/docs/interfaces/AssetBatchRequest.html +2 -0
  81. package/docs/interfaces/AssetBatchResponse.html +3 -0
  82. package/docs/interfaces/AssetBatchRestoreOperation.html +4 -0
  83. package/docs/interfaces/AssetBatchUpsertItem.html +10 -0
  84. package/docs/interfaces/AssetBatchUpsertOperation.html +4 -0
  85. package/docs/interfaces/AssetGet.html +16 -0
  86. package/docs/interfaces/AssetListItem.html +11 -0
  87. package/docs/interfaces/AssetPosition.html +4 -0
  88. package/docs/interfaces/AssetPost.html +10 -0
  89. package/docs/interfaces/AssetPut.html +10 -0
  90. package/docs/interfaces/BatchItemError.html +4 -0
  91. package/docs/interfaces/BatchItemResult.html +6 -0
  92. package/docs/interfaces/BatchSummary.html +4 -0
  93. package/docs/interfaces/Diagnostics.html +4 -0
  94. package/docs/interfaces/DiagnosticsSummary.html +4 -0
  95. package/docs/interfaces/ElementQuantities.html +3 -0
  96. package/docs/interfaces/EntityCollection.html +10 -0
  97. package/docs/interfaces/Facility.html +15 -0
  98. package/docs/interfaces/FacilityPart.html +14 -0
  99. package/docs/interfaces/FilePatch.html +7 -0
  100. package/docs/interfaces/FilePatchSet.html +5 -0
  101. package/docs/interfaces/GenericDiagnosticEntry.html +4 -0
  102. package/docs/interfaces/ITwinClient.html +34 -0
  103. package/docs/interfaces/IfcDiagnosticEntry.html +5 -0
  104. package/docs/interfaces/IfcDiagnosticNode.html +6 -0
  105. package/docs/interfaces/IfcDiagnosticNodeData.html +6 -0
  106. package/docs/interfaces/ListTwinItem.html +14 -0
  107. package/docs/interfaces/Location.html +10 -0
  108. package/docs/interfaces/PaginatedResponse.html +3 -0
  109. package/docs/interfaces/Pagination.html +4 -0
  110. package/docs/interfaces/Property.html +4 -0
  111. package/docs/interfaces/PropertySet.html +6 -0
  112. package/docs/interfaces/SimpleQuantity.html +6 -0
  113. package/docs/interfaces/SinglePropertyValue.html +5 -0
  114. package/docs/interfaces/Site.html +18 -0
  115. package/docs/interfaces/Space.html +11 -0
  116. package/docs/interfaces/TwinComputedPropertySetsResponse.html +4 -0
  117. package/docs/interfaces/TwinContentResponse.html +6 -0
  118. package/docs/interfaces/TwinDelete.html +2 -0
  119. package/docs/interfaces/TwinDiagnosticEntry.html +5 -0
  120. package/docs/interfaces/TwinDiagnosticNode.html +4 -0
  121. package/docs/interfaces/TwinDiagnosticNodeData.html +9 -0
  122. package/docs/interfaces/TwinDiagnosticsResponse.html +5 -0
  123. package/docs/interfaces/TwinErrorBody.html +4 -0
  124. package/docs/interfaces/TwinFileDelete.html +2 -0
  125. package/docs/interfaces/TwinFileGet.html +15 -0
  126. package/docs/interfaces/TwinFileListOptions.html +6 -0
  127. package/docs/interfaces/TwinFilePut.html +8 -0
  128. package/docs/interfaces/TwinFileReference.html +4 -0
  129. package/docs/interfaces/TwinFileRestore.html +2 -0
  130. package/docs/interfaces/TwinFileSource.html +4 -0
  131. package/docs/interfaces/TwinFileTransform.html +11 -0
  132. package/docs/interfaces/TwinFileVersionListOptions.html +4 -0
  133. package/docs/interfaces/TwinGet.html +30 -0
  134. package/docs/interfaces/TwinProcessorEvent.html +7 -0
  135. package/docs/interfaces/TwinProcessorResponse.html +5 -0
  136. package/docs/interfaces/TwinPut.html +23 -0
  137. package/docs/interfaces/TwinSpecification.html +20 -0
  138. package/docs/interfaces/TwinVersionContentResponse.html +7 -0
  139. package/docs/types/AssetBatchGetStatus.html +1 -0
  140. package/docs/types/AssetBatchOperation.html +1 -0
  141. package/docs/types/AssetBatchOperationStatus.html +1 -0
  142. package/docs/types/DiagnosticCategory.html +2 -0
  143. package/docs/types/DiagnosticLevel.html +2 -0
  144. package/docs/types/EpsgType.html +1 -0
  145. package/docs/types/FacilityClass.html +3 -0
  146. package/docs/types/FacilityPartClass.html +3 -0
  147. package/docs/types/IfcDiagnosticCode.html +1 -0
  148. package/docs/types/Op.html +2 -0
  149. package/docs/types/SinglePropertyValueType.html +3 -0
  150. package/docs/types/SingleQuantityValueType.html +3 -0
  151. package/docs/types/SiteType.html +3 -0
  152. package/docs/types/SpaceType.html +2 -0
  153. package/docs/types/TwinDiagnosticCode.html +1 -0
  154. package/package.json +56 -0
  155. package/src/BuildInfo.ts +41 -0
  156. package/src/TwinClient.ts +1274 -0
  157. package/src/errors.ts +107 -0
  158. package/src/index.ts +3 -0
  159. package/src/tsconfig.json +13 -0
  160. package/src/types/Twin.ts +7 -0
  161. package/src/types/TwinGeometryInfo.ts +7 -0
  162. package/src/types/asset-api.ts +60 -0
  163. package/src/types/asset-batch-api.ts +88 -0
  164. package/src/types/common.ts +10 -0
  165. package/src/types/generated.ts +574 -0
  166. package/src/types/index.ts +6 -0
  167. package/src/types/twin-api.ts +99 -0
  168. package/src/types/twin-file-api.ts +67 -0
@@ -0,0 +1,1274 @@
1
+ import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
2
+ import {
3
+ readT8ClientModelWithFactories,
4
+ readT8GIModelWithFactories,
5
+ T8ClientEntityTypeDTO,
6
+ T8ClientModel,
7
+ T8ClientModelDTO,
8
+ T8ClientProductConnectionsDTO,
9
+ T8ClientProductDTO,
10
+ T8ClientProjectConnectionsDTO,
11
+ T8ClientProjectDTO,
12
+ T8ClientUnitDTO,
13
+ T8EntityType,
14
+ T8GIMeshDescriptor,
15
+ T8GIMeshDescriptorDTO,
16
+ T8GIStyle,
17
+ T8GIStyleDTO,
18
+ T8GIRegion,
19
+ T8GIRegionDTO,
20
+ T8Product,
21
+ T8Project,
22
+ T8Unit
23
+ } from '@twinfinity/t8-client-model';
24
+ import type { T8GIModelGeneric } from '@twinfinity/t8-client-model';
25
+ import type {
26
+ PaginatedResponse,
27
+ ListTwinItem,
28
+ TwinGet,
29
+ TwinPut,
30
+ TwinVersionContentResponse,
31
+ TwinProcessorEvent,
32
+ TwinDiagnosticsResponse,
33
+ AssetListItem,
34
+ AssetGet,
35
+ AssetPost,
36
+ AssetPut,
37
+ AssetBatchOperation,
38
+ AssetBatchResponse,
39
+ AssetBatchGetResponse,
40
+ TwinFileGet,
41
+ TwinFilePut,
42
+ TwinFileDelete,
43
+ TwinFileListOptions,
44
+ TwinFileVersionListOptions,
45
+ TwinContentResponse
46
+ } from './types';
47
+ import {
48
+ TwinValidationError,
49
+ TwinConflictError,
50
+ TwinNotFoundError,
51
+ ProcessorNotFoundError,
52
+ AssetValidationError,
53
+ AssetConflictError,
54
+ AssetNotFoundError,
55
+ AssetBatchError,
56
+ TwinFileValidationError,
57
+ TwinFileConflictError,
58
+ TwinFileNotFoundError
59
+ } from './errors';
60
+ import { Twin } from './types/Twin';
61
+ import { TwinGeometryInfo } from './types/TwinGeometryInfo';
62
+
63
+ export interface ITwinClient {
64
+ listTwins(
65
+ relatedTo?: string,
66
+ page?: string,
67
+ limit?: number,
68
+ signal?: AbortSignal
69
+ ): Promise<PaginatedResponse<ListTwinItem>>;
70
+
71
+ listAllTwins(relatedTo?: string, signal?: AbortSignal): Promise<ListTwinItem[]>;
72
+
73
+ getTwin(twin: string | { id: string; version: number }, signal?: AbortSignal): Promise<TwinGet>;
74
+
75
+ listTwinVersions(
76
+ id: string,
77
+ options?: {
78
+ includeDeleted?: boolean;
79
+ page?: string;
80
+ limit?: number;
81
+ },
82
+ signal?: AbortSignal
83
+ ): Promise<PaginatedResponse<ListTwinItem>>;
84
+
85
+ downloadTwinContent(
86
+ twinId: string,
87
+ twinVersion: number,
88
+ content: TwinContentResponse,
89
+ signal?: AbortSignal
90
+ ): Promise<ArrayBuffer>;
91
+
92
+ loadTwin(
93
+ twin: string | { id: string; version: number },
94
+ signal?: AbortSignal
95
+ ): Promise<Twin<T8Project, T8Product<T8EntityType>, T8EntityType, T8Unit>>;
96
+
97
+ loadTwinWithFactories<TProject, TProduct, TEntityType, TUnit>(
98
+ twin: string | { id: string; version: number },
99
+ projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
100
+ productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
101
+ entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
102
+ unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
103
+ connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
104
+ connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
105
+ signal?: AbortSignal
106
+ ): Promise<Twin<TProject, TProduct, TEntityType, TUnit>>;
107
+
108
+ loadGeometryInfo(
109
+ twin: string | { id: string; version: number },
110
+ signal?: AbortSignal
111
+ ): Promise<TwinGeometryInfo<T8GIMeshDescriptor, T8GIStyle, T8GIRegion>>;
112
+
113
+ loadGeometryInfoWithFactories<TMeshDescriptor, TStyle, TRegion>(
114
+ twin: string | { id: string; version: number },
115
+ meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
116
+ styleFactory: (dto: T8GIStyleDTO) => TStyle,
117
+ regionFactory: (dto: T8GIRegionDTO) => TRegion,
118
+ signal?: AbortSignal
119
+ ): Promise<TwinGeometryInfo<TMeshDescriptor, TStyle, TRegion>>;
120
+
121
+ createOrUpdateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet>;
122
+ updateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet>;
123
+
124
+ restoreTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet>;
125
+
126
+ getTwinVersionContent(
127
+ twinId: string,
128
+ twinVersion: number,
129
+ signal?: AbortSignal
130
+ ): Promise<TwinVersionContentResponse>;
131
+ getTwinContent(twinId: string, signal?: AbortSignal): Promise<TwinVersionContentResponse>;
132
+
133
+ getTwinDiagnostics(twinId: string, signal?: AbortSignal): Promise<TwinDiagnosticsResponse>;
134
+ getTwinVersionDiagnostics(
135
+ twinId: string,
136
+ twinVersion: number,
137
+ signal?: AbortSignal
138
+ ): Promise<TwinDiagnosticsResponse>;
139
+
140
+ getTwinVersionEvents(
141
+ twinId: string,
142
+ twinVersion: number,
143
+ offset?: number,
144
+ limit?: number,
145
+ signal?: AbortSignal
146
+ ): Promise<PaginatedResponse<TwinProcessorEvent>>;
147
+
148
+ getProcessorEvents(
149
+ twinId: string,
150
+ twinVersion: number,
151
+ processorId: string,
152
+ offset?: number,
153
+ limit?: number,
154
+ signal?: AbortSignal
155
+ ): Promise<PaginatedResponse<TwinProcessorEvent>>;
156
+
157
+ listAssets(
158
+ twinId: string,
159
+ offset?: number,
160
+ limit?: number,
161
+ options?: { includeDeleted?: boolean },
162
+ signal?: AbortSignal
163
+ ): Promise<PaginatedResponse<AssetListItem>>;
164
+
165
+ getAsset(twinId: string, assetId: string, signal?: AbortSignal): Promise<AssetGet>;
166
+
167
+ createAssets(twinId: string, assets: AssetPost[], signal?: AbortSignal): Promise<AssetGet[]>;
168
+
169
+ updateAsset(twinId: string, assetId: string, asset: AssetPut, signal?: AbortSignal): Promise<AssetGet>;
170
+
171
+ deleteAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet>;
172
+
173
+ deleteTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet>;
174
+
175
+ listTwinFiles(
176
+ twinId: string,
177
+ options?: TwinFileListOptions,
178
+ signal?: AbortSignal
179
+ ): Promise<PaginatedResponse<TwinFileGet>>;
180
+ getTwinFile(twinId: string, fileId: string, signal?: AbortSignal): Promise<TwinFileGet>;
181
+
182
+ createOrUpdateTwinFile(
183
+ twinId: string,
184
+ fileId: string,
185
+ twinFile: TwinFilePut,
186
+ signal?: AbortSignal
187
+ ): Promise<TwinFileGet>;
188
+ deleteTwinFile(
189
+ twinId: string,
190
+ fileId: string,
191
+ deleteRequest: TwinFileDelete,
192
+ signal?: AbortSignal
193
+ ): Promise<TwinFileGet>;
194
+
195
+ restoreTwinFile(twinId: string, fileId: string, etag: string, signal?: AbortSignal): Promise<TwinFileGet>;
196
+ getTwinFileContent(twinId: string, fileId: string, signal?: AbortSignal): Promise<Blob>;
197
+
198
+ getTwinFileVersion(twinId: string, fileId: string, version: number, signal?: AbortSignal): Promise<TwinFileGet>;
199
+ listTwinFileVersions(
200
+ twinId: string,
201
+ fileId: string,
202
+ options?: TwinFileVersionListOptions,
203
+ signal?: AbortSignal
204
+ ): Promise<PaginatedResponse<TwinFileGet>>;
205
+
206
+ getTwinFileVersionContent(twinId: string, fileId: string, version: number, signal?: AbortSignal): Promise<Blob>;
207
+ }
208
+
209
+ export class TwinClient implements ITwinClient {
210
+ private readonly _baseUrl: string;
211
+
212
+ constructor(
213
+ baseUrl: string | URL,
214
+ private readonly _httpClient: TwinfinityHttpClient
215
+ ) {
216
+ this._baseUrl = `${baseUrl}/_ps/twins`;
217
+ }
218
+
219
+ async listTwins(
220
+ relatedTo?: string,
221
+ page: string | undefined = undefined,
222
+ limit = 100,
223
+ signal?: AbortSignal
224
+ ): Promise<PaginatedResponse<ListTwinItem>> {
225
+ const params = new URLSearchParams();
226
+ if (relatedTo) params.set('relatedTo', relatedTo);
227
+ if (page !== undefined) params.set('page', page);
228
+ if (limit !== 100) params.set('limit', limit.toString());
229
+
230
+ const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
231
+
232
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
233
+ headers: { 'Content-Type': 'application/json' },
234
+ signal
235
+ });
236
+
237
+ if (response.ok) {
238
+ return (await response.json()) as PaginatedResponse<ListTwinItem>;
239
+ }
240
+
241
+ if (response.status === 400) {
242
+ const errorBody = await response.json();
243
+ throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
244
+ }
245
+
246
+ const errorText = await response.text();
247
+ throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
248
+ }
249
+
250
+ async listAllTwins(relatedTo?: string, signal?: AbortSignal): Promise<ListTwinItem[]> {
251
+ const allTwins: ListTwinItem[] = [];
252
+ let page: string | undefined = undefined;
253
+ const limit = 100;
254
+ const maxPages = 1000;
255
+
256
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
257
+ const response = await this.listTwins(relatedTo, page, limit, signal);
258
+ allTwins.push(...response.data);
259
+
260
+ if (response.data.length < limit) {
261
+ break;
262
+ }
263
+
264
+ page = response.pagination.nextPage;
265
+ }
266
+
267
+ return allTwins;
268
+ }
269
+
270
+ async getTwin(twin: string | { id: string; version: number }, signal?: AbortSignal): Promise<TwinGet> {
271
+ const twinId = typeof twin === 'string' ? twin : twin.id;
272
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
273
+ const response = await this._httpClient.fetch(
274
+ HttpMethod.Get,
275
+ `${this._baseUrl}/${twinId}${specifiedVersion !== undefined ? `/versions/${specifiedVersion}` : ''}`,
276
+ {
277
+ headers: { 'Content-Type': 'application/json' },
278
+ signal
279
+ }
280
+ );
281
+
282
+ if (response.ok) {
283
+ return (await response.json()) as TwinGet;
284
+ }
285
+
286
+ if (response.status === 404) {
287
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
288
+ }
289
+
290
+ const errorText = await response.text();
291
+ throw new Error(`Failed to get twin: ${response.status} ${response.statusText} - ${errorText}`);
292
+ }
293
+
294
+ async listTwinVersions(
295
+ id: string,
296
+ options?: {
297
+ includeDeleted?: boolean;
298
+ page: string | undefined;
299
+ limit?: number;
300
+ },
301
+ signal?: AbortSignal
302
+ ): Promise<PaginatedResponse<ListTwinItem>> {
303
+ const params = new URLSearchParams();
304
+ if (options?.page !== undefined) params.set('page', options.page);
305
+ if (options?.limit !== undefined && options.limit !== 100) params.set('limit', options.limit.toString());
306
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
307
+
308
+ const url = `${this._baseUrl}/${id}/versions${params.toString() ? `?${params}` : ''}`;
309
+
310
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
311
+ headers: { 'Content-Type': 'application/json' },
312
+ signal
313
+ });
314
+
315
+ if (response.ok) {
316
+ return (await response.json()) as PaginatedResponse<ListTwinItem>;
317
+ }
318
+
319
+ if (response.status === 400) {
320
+ const errorBody = await response.json();
321
+ throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
322
+ }
323
+
324
+ const errorText = await response.text();
325
+ throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
326
+ }
327
+
328
+ async downloadTwinContent(
329
+ twinId: string,
330
+ twinVersion: number,
331
+ content: TwinContentResponse,
332
+ signal?: AbortSignal
333
+ ): Promise<ArrayBuffer> {
334
+ const response = await this._httpClient.fetch(
335
+ HttpMethod.Get,
336
+ `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents/${content.id}/content`,
337
+ { signal }
338
+ );
339
+ if (response.ok) {
340
+ const blob = await response.blob();
341
+ return await blob.arrayBuffer();
342
+ }
343
+
344
+ throw new Error(`Failed to download twin content: ${response.status} ${response.statusText}`);
345
+ }
346
+
347
+ private async loadTwinVersion<TProject, TProduct, TEntityType, TUnit>(
348
+ twinId: string,
349
+ twinVersion: number,
350
+ projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
351
+ productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
352
+ entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
353
+ unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
354
+ connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
355
+ connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
356
+ signal?: AbortSignal
357
+ ): Promise<T8ClientModel<TProject, TProduct, TEntityType, TUnit> | undefined> {
358
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
359
+
360
+ const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
361
+
362
+ if (t8cmItem === undefined) {
363
+ return undefined;
364
+ }
365
+
366
+ const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
367
+
368
+ const data = await Promise.all(
369
+ [this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal)].concat(
370
+ t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal))
371
+ )
372
+ );
373
+
374
+ return readT8ClientModelWithFactories<TProject, TProduct, TEntityType, TUnit>(
375
+ data,
376
+ projectFactory,
377
+ productFactory,
378
+ entityTypeFactory,
379
+ unitFactory,
380
+ connectProject,
381
+ connectProduct
382
+ );
383
+ }
384
+
385
+ loadTwin(
386
+ twin: string | { id: string; version: number },
387
+ signal?: AbortSignal
388
+ ): Promise<Twin<T8Project, T8Product<T8EntityType>, T8EntityType, T8Unit>> {
389
+ return this.loadTwinWithFactories(
390
+ twin,
391
+ T8Project.create<T8Product<T8EntityType>, T8Unit>,
392
+ T8Product.create<T8EntityType, T8Unit>,
393
+ T8EntityType.create,
394
+ T8Unit.create,
395
+ T8Project.connect,
396
+ T8Product.connect,
397
+ signal
398
+ );
399
+ }
400
+
401
+ async loadTwinWithFactories<TProject, TProduct, TEntityType, TUnit>(
402
+ twin: string | { id: string; version: number },
403
+ projectFactory: (dto: T8ClientProjectDTO<TUnit>) => TProject,
404
+ productFactory: (dto: T8ClientProductDTO<TUnit>) => TProduct,
405
+ entityTypeFactory: (dto: T8ClientEntityTypeDTO<TUnit>) => TEntityType,
406
+ unitFactory: (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO) => TUnit,
407
+ connectProject: (dto: T8ClientProjectConnectionsDTO<TProduct>, project: TProject) => void,
408
+ connectProduct: (dto: T8ClientProductConnectionsDTO<TProduct, TEntityType>, product: TProduct) => void,
409
+ signal?: AbortSignal
410
+ ): Promise<Twin<TProject, TProduct, TEntityType, TUnit>> {
411
+ let page: string | undefined = undefined;
412
+ const twinId = typeof twin === 'string' ? twin : twin.id;
413
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
414
+ if (specifiedVersion !== undefined) {
415
+ return {
416
+ id: twinId,
417
+ version: specifiedVersion,
418
+ model: await this.loadTwinVersion(
419
+ twinId,
420
+ specifiedVersion,
421
+ projectFactory,
422
+ productFactory,
423
+ entityTypeFactory,
424
+ unitFactory,
425
+ connectProject,
426
+ connectProduct,
427
+ signal
428
+ )
429
+ };
430
+ }
431
+
432
+ const maxPages = 100;
433
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
434
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
435
+ if (versions.data.length === 0) {
436
+ break;
437
+ }
438
+ for (const versionItem of versions.data) {
439
+ const loadedModel = await this.loadTwinVersion(
440
+ twinId,
441
+ versionItem.version,
442
+ projectFactory,
443
+ productFactory,
444
+ entityTypeFactory,
445
+ unitFactory,
446
+ connectProject,
447
+ connectProduct,
448
+ signal
449
+ );
450
+ if (loadedModel !== undefined) {
451
+ return {
452
+ id: twinId,
453
+ version: versionItem.version,
454
+ model: loadedModel
455
+ };
456
+ }
457
+ }
458
+ page = versions.pagination.nextPage;
459
+ }
460
+
461
+ throw new Error(`Twin ${twinId} does not have a client model`);
462
+ }
463
+
464
+ private async loadGeometryInfoVersion<TMeshDescriptor, TStyle, TRegion>(
465
+ twinId: string,
466
+ twinVersion: number,
467
+ meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
468
+ styleFactory: (dto: T8GIStyleDTO) => TStyle,
469
+ regionFactory: (dto: T8GIRegionDTO) => TRegion,
470
+ signal?: AbortSignal
471
+ ): Promise<T8GIModelGeneric<TMeshDescriptor, TStyle, TRegion> | undefined> {
472
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
473
+
474
+ const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
475
+
476
+ if (t8giItem === undefined) {
477
+ return undefined;
478
+ }
479
+
480
+ const data = await this.downloadTwinContent(twinId, twinVersion, t8giItem, signal);
481
+
482
+ return readT8GIModelWithFactories(data, meshDescriptorFactory, styleFactory, regionFactory);
483
+ }
484
+
485
+ loadGeometryInfo(
486
+ twin: string | { id: string; version: number },
487
+ signal?: AbortSignal
488
+ ): Promise<TwinGeometryInfo<T8GIMeshDescriptor, T8GIStyle, T8GIRegion>> {
489
+ return this.loadGeometryInfoWithFactories(
490
+ twin,
491
+ T8GIMeshDescriptor.create,
492
+ T8GIStyle.create,
493
+ T8GIRegion.create,
494
+ signal
495
+ );
496
+ }
497
+
498
+ async loadGeometryInfoWithFactories<TMeshDescriptor, TStyle, TRegion>(
499
+ twin: string | { id: string; version: number },
500
+ meshDescriptorFactory: (dto: T8GIMeshDescriptorDTO) => TMeshDescriptor,
501
+ styleFactory: (dto: T8GIStyleDTO) => TStyle,
502
+ regionFactory: (dto: T8GIRegionDTO) => TRegion,
503
+ signal?: AbortSignal
504
+ ): Promise<TwinGeometryInfo<TMeshDescriptor, TStyle, TRegion>> {
505
+ let page: string | undefined = undefined;
506
+ const twinId = typeof twin === 'string' ? twin : twin.id;
507
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
508
+
509
+ if (specifiedVersion !== undefined) {
510
+ return {
511
+ id: twinId,
512
+ version: specifiedVersion,
513
+ model: await this.loadGeometryInfoVersion(
514
+ twinId,
515
+ specifiedVersion,
516
+ meshDescriptorFactory,
517
+ styleFactory,
518
+ regionFactory,
519
+ signal
520
+ )
521
+ };
522
+ }
523
+
524
+ const maxPages = 100;
525
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
526
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
527
+ if (versions.data.length === 0) {
528
+ break;
529
+ }
530
+ for (const versionItem of versions.data) {
531
+ const model = await this.loadGeometryInfoVersion(
532
+ twinId,
533
+ versionItem.version,
534
+ meshDescriptorFactory,
535
+ styleFactory,
536
+ regionFactory,
537
+ signal
538
+ );
539
+ if (model !== undefined) {
540
+ return {
541
+ id: twinId,
542
+ version: versionItem.version,
543
+ model
544
+ };
545
+ }
546
+ }
547
+ page = versions.pagination.nextPage;
548
+ }
549
+
550
+ throw new Error(`Twin ${twinId} does not have geometry info`);
551
+ }
552
+
553
+ async createOrUpdateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet> {
554
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
555
+ headers: { 'Content-Type': 'application/json' },
556
+ body: JSON.stringify(twin),
557
+ signal
558
+ });
559
+
560
+ if (response.ok) {
561
+ return (await response.json()) as TwinGet;
562
+ }
563
+
564
+ if (response.status === 400) {
565
+ const errorBody = await response.json();
566
+ throw new TwinValidationError(`Twin validation failed: ${errorBody.detail}`, errorBody);
567
+ }
568
+
569
+ if (response.status === 409) {
570
+ const conflictedTwin = (await response.json()) as TwinGet;
571
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
572
+ }
573
+
574
+ const errorText = await response.text();
575
+ throw new Error(`Failed to create/update twin: ${response.status} ${response.statusText} - ${errorText}`);
576
+ }
577
+
578
+ async updateTwin(twinId: string, twin: TwinPut, signal?: AbortSignal): Promise<TwinGet> {
579
+ return this.createOrUpdateTwin(twinId, twin, signal);
580
+ }
581
+
582
+ async deleteTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet> {
583
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}`, {
584
+ headers: { 'Content-Type': 'application/json' },
585
+ body: JSON.stringify({ etag }),
586
+ signal
587
+ });
588
+
589
+ if (response.ok) {
590
+ return (await response.json()) as TwinGet;
591
+ }
592
+
593
+ if (response.status === 404) {
594
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
595
+ }
596
+
597
+ if (response.status === 409) {
598
+ const conflictedTwin = (await response.json()) as TwinGet;
599
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
600
+ }
601
+
602
+ const errorText = await response.text();
603
+ throw new Error(`Failed to delete twin: ${response.status} ${response.statusText} - ${errorText}`);
604
+ }
605
+
606
+ async restoreTwin(twinId: string, etag: string, signal?: AbortSignal): Promise<TwinGet> {
607
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/restore`, {
608
+ headers: { 'Content-Type': 'application/json' },
609
+ body: JSON.stringify({ etag }),
610
+ signal
611
+ });
612
+
613
+ if (response.ok) {
614
+ return (await response.json()) as TwinGet;
615
+ }
616
+
617
+ if (response.status === 404) {
618
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
619
+ }
620
+
621
+ if (response.status === 409) {
622
+ const conflictedTwin = (await response.json()) as TwinGet;
623
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
624
+ }
625
+
626
+ const errorText = await response.text();
627
+ throw new Error(`Failed to restore twin: ${response.status} ${response.statusText} - ${errorText}`);
628
+ }
629
+
630
+ async getTwinVersionContent(
631
+ twinId: string,
632
+ twinVersion: number,
633
+ signal?: AbortSignal
634
+ ): Promise<TwinVersionContentResponse> {
635
+ const response = await this._httpClient.fetch(
636
+ HttpMethod.Get,
637
+ `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`,
638
+ { headers: { 'Content-Type': 'application/json' }, signal }
639
+ );
640
+
641
+ if (response.ok) {
642
+ return (await response.json()) as TwinVersionContentResponse;
643
+ }
644
+
645
+ if (response.status === 404) {
646
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
647
+ }
648
+
649
+ const errorText = await response.text();
650
+ throw new Error(`Failed to get twin version content: ${response.status} ${response.statusText} - ${errorText}`);
651
+ }
652
+
653
+ async getTwinContent(twinId: string, signal?: AbortSignal): Promise<TwinVersionContentResponse> {
654
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/contents`, {
655
+ headers: { 'Content-Type': 'application/json' },
656
+ signal
657
+ });
658
+
659
+ if (response.ok) {
660
+ return (await response.json()) as TwinVersionContentResponse;
661
+ }
662
+
663
+ if (response.status === 404) {
664
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
665
+ }
666
+
667
+ const errorText = await response.text();
668
+ throw new Error(`Failed to get twin content: ${response.status} ${response.statusText} - ${errorText}`);
669
+ }
670
+
671
+ async getTwinDiagnostics(twinId: string, signal?: AbortSignal): Promise<TwinDiagnosticsResponse> {
672
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/diagnostics`, {
673
+ headers: { 'Content-Type': 'application/json' },
674
+ signal
675
+ });
676
+
677
+ if (response.ok) {
678
+ return (await response.json()) as TwinDiagnosticsResponse;
679
+ }
680
+
681
+ if (response.status === 404) {
682
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
683
+ }
684
+
685
+ const errorText = await response.text();
686
+ throw new Error(`Failed to get twin diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
687
+ }
688
+
689
+ async getTwinVersionDiagnostics(
690
+ twinId: string,
691
+ twinVersion: number,
692
+ signal?: AbortSignal
693
+ ): Promise<TwinDiagnosticsResponse> {
694
+ const response = await this._httpClient.fetch(
695
+ HttpMethod.Get,
696
+ `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`,
697
+ { headers: { 'Content-Type': 'application/json' }, signal }
698
+ );
699
+
700
+ if (response.ok) {
701
+ return (await response.json()) as TwinDiagnosticsResponse;
702
+ }
703
+
704
+ if (response.status === 404) {
705
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
706
+ }
707
+
708
+ const errorText = await response.text();
709
+ throw new Error(
710
+ `Failed to get twin version diagnostics: ${response.status} ${response.statusText} - ${errorText}`
711
+ );
712
+ }
713
+
714
+ async getTwinVersionEvents(
715
+ twinId: string,
716
+ twinVersion: number,
717
+ offset = 0,
718
+ limit = 100,
719
+ signal?: AbortSignal
720
+ ): Promise<PaginatedResponse<TwinProcessorEvent>> {
721
+ const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/events?offset=${offset}&limit=${limit}`;
722
+
723
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
724
+ headers: { 'Content-Type': 'application/json' },
725
+ signal
726
+ });
727
+
728
+ if (response.ok) {
729
+ return (await response.json()) as PaginatedResponse<TwinProcessorEvent>;
730
+ }
731
+
732
+ if (response.status === 400) {
733
+ const errorBody = await response.json();
734
+ throw new TwinValidationError(`Twin events validation failed: ${errorBody.detail}`, errorBody);
735
+ }
736
+
737
+ if (response.status === 404) {
738
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
739
+ }
740
+
741
+ const errorText = await response.text();
742
+ throw new Error(`Failed to get twin version events: ${response.status} ${response.statusText} - ${errorText}`);
743
+ }
744
+
745
+ async getProcessorEvents(
746
+ twinId: string,
747
+ twinVersion: number,
748
+ processorId: string,
749
+ offset = 0,
750
+ limit = 100,
751
+ signal?: AbortSignal
752
+ ): Promise<PaginatedResponse<TwinProcessorEvent>> {
753
+ const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/processors/${processorId}/events?offset=${offset}&limit=${limit}`;
754
+
755
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
756
+ headers: { 'Content-Type': 'application/json' },
757
+ signal
758
+ });
759
+
760
+ if (response.ok) {
761
+ return (await response.json()) as PaginatedResponse<TwinProcessorEvent>;
762
+ }
763
+
764
+ if (response.status === 400) {
765
+ const errorBody = await response.json();
766
+ throw new TwinValidationError(`Processor events validation failed: ${errorBody.detail}`, errorBody);
767
+ }
768
+
769
+ if (response.status === 404) {
770
+ throw new ProcessorNotFoundError(
771
+ `Processor ${processorId} not found for twin ${twinId} version ${twinVersion}`
772
+ );
773
+ }
774
+
775
+ const errorText = await response.text();
776
+ throw new Error(`Failed to get processor events: ${response.status} ${response.statusText} - ${errorText}`);
777
+ }
778
+
779
+ async listAssets(
780
+ twinId: string,
781
+ offset = 0,
782
+ limit = 100,
783
+ options?: { includeDeleted?: boolean },
784
+ signal?: AbortSignal
785
+ ): Promise<PaginatedResponse<AssetListItem>> {
786
+ const includeDeleted = options?.includeDeleted ?? false;
787
+ const params = `offset=${offset}&limit=${limit}${includeDeleted ? '&includeDeleted=true' : ''}`;
788
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets?${params}`, {
789
+ headers: { 'Content-Type': 'application/json' },
790
+ signal
791
+ });
792
+
793
+ if (!response.ok) {
794
+ const errorText = await response.text();
795
+ throw new Error(`Failed to list assets: ${response.status} ${response.statusText} - ${errorText}`);
796
+ }
797
+
798
+ return (await response.json()) as PaginatedResponse<AssetListItem>;
799
+ }
800
+
801
+ async getAsset(twinId: string, assetId: string, signal?: AbortSignal): Promise<AssetGet> {
802
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
803
+ headers: { 'Content-Type': 'application/json' },
804
+ signal
805
+ });
806
+
807
+ if (response.ok) {
808
+ return (await response.json()) as AssetGet;
809
+ }
810
+
811
+ if (response.status === 404) {
812
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
813
+ }
814
+
815
+ const errorText = await response.text();
816
+ throw new Error(`Failed to get asset: ${response.status} ${response.statusText} - ${errorText}`);
817
+ }
818
+
819
+ async createAssets(twinId: string, assets: AssetPost[], signal?: AbortSignal): Promise<AssetGet[]> {
820
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets`, {
821
+ headers: { 'Content-Type': 'application/json' },
822
+ body: JSON.stringify(assets),
823
+ signal
824
+ });
825
+
826
+ if (response.status === 201) {
827
+ return (await response.json()) as AssetGet[];
828
+ }
829
+
830
+ if (response.status === 400) {
831
+ const errorText = await response.text();
832
+ throw new AssetValidationError(`Asset validation failed: ${errorText}`);
833
+ }
834
+
835
+ const errorText = await response.text();
836
+ throw new Error(`Failed to create assets: ${response.status} ${response.statusText} - ${errorText}`);
837
+ }
838
+
839
+ async updateAsset(twinId: string, assetId: string, asset: AssetPut, signal?: AbortSignal): Promise<AssetGet> {
840
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
841
+ headers: { 'Content-Type': 'application/json' },
842
+ body: JSON.stringify(asset),
843
+ signal
844
+ });
845
+
846
+ if (response.ok) {
847
+ return (await response.json()) as AssetGet;
848
+ }
849
+
850
+ if (response.status === 409) {
851
+ const conflictedAsset = (await response.json()) as AssetGet;
852
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
853
+ }
854
+
855
+ if (response.status === 400) {
856
+ const errorText = await response.text();
857
+ throw new AssetValidationError(`Asset validation failed: ${errorText}`);
858
+ }
859
+
860
+ const errorText = await response.text();
861
+ throw new Error(`Failed to update asset: ${response.status} ${response.statusText} - ${errorText}`);
862
+ }
863
+
864
+ async deleteAsset(twinId: string, assetId: string, etag: string, signal?: AbortSignal): Promise<AssetGet> {
865
+ const response = await this._httpClient.fetch(
866
+ HttpMethod.Delete,
867
+ `${this._baseUrl}/${twinId}/assets/${assetId}`,
868
+ {
869
+ headers: { 'Content-Type': 'application/json' },
870
+ body: JSON.stringify({ etag }),
871
+ signal
872
+ }
873
+ );
874
+
875
+ if (response.ok) {
876
+ return (await response.json()) as AssetGet;
877
+ }
878
+
879
+ if (response.status === 409) {
880
+ const conflictedAsset = (await response.json()) as AssetGet;
881
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
882
+ }
883
+
884
+ if (response.status === 404) {
885
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
886
+ }
887
+
888
+ const errorText = await response.text();
889
+ throw new Error(`Failed to delete asset: ${response.status} ${response.statusText} - ${errorText}`);
890
+ }
891
+
892
+ /**
893
+ * Restores a soft-deleted asset.
894
+ *
895
+ * @param twinId - The twin ID
896
+ * @param assetId - The asset ID to restore
897
+ * @param etag - The current etag of the deleted asset
898
+ * @returns The restored asset
899
+ * @throws {AssetConflictError} When the asset has been modified (409)
900
+ * @throws {AssetNotFoundError} When the asset does not exist (404)
901
+ */
902
+ async restoreAsset(twinId: string, assetId: string, etag: string): Promise<AssetGet> {
903
+ const response = await this._httpClient.fetch(
904
+ HttpMethod.Put,
905
+ `${this._baseUrl}/${twinId}/assets/${assetId}/restore`,
906
+ {
907
+ headers: { 'Content-Type': 'application/json' },
908
+ body: JSON.stringify({ etag })
909
+ }
910
+ );
911
+
912
+ if (response.ok) {
913
+ return (await response.json()) as AssetGet;
914
+ }
915
+
916
+ if (response.status === 409) {
917
+ const conflictedAsset = (await response.json()) as AssetGet;
918
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
919
+ }
920
+
921
+ if (response.status === 404) {
922
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
923
+ }
924
+
925
+ const errorText = await response.text();
926
+ throw new Error(`Failed to restore asset: ${response.status} ${response.statusText} - ${errorText}`);
927
+ }
928
+
929
+ /**
930
+ * Executes a batch of asset operations (upsert, delete, restore) atomically.
931
+ * All operations succeed or all are rolled back.
932
+ *
933
+ * @param twinId - The twin ID
934
+ * @param operations - Array of batch operations (max 1000)
935
+ * @returns Batch response with results for each operation
936
+ * @throws {AssetValidationError} When request validation fails (400) or insufficient permissions (403)
937
+ * @throws {AssetBatchError} When an operation conflicts (409) or asset not found (404) — the response contains per-item results with reverted statuses
938
+ */
939
+ async batchAssets(twinId: string, operations: AssetBatchOperation[]): Promise<AssetBatchResponse> {
940
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch`, {
941
+ headers: { 'Content-Type': 'application/json' },
942
+ body: JSON.stringify({ operations })
943
+ });
944
+
945
+ if (response.ok) {
946
+ return (await response.json()) as AssetBatchResponse;
947
+ }
948
+
949
+ if (response.status === 400) {
950
+ const errorText = await response.text();
951
+ throw new AssetValidationError(`Asset batch validation failed: ${errorText}`);
952
+ }
953
+
954
+ if (response.status === 403) {
955
+ const errorText = await response.text();
956
+ throw new AssetValidationError(`Asset batch forbidden: ${errorText}`);
957
+ }
958
+
959
+ if (response.status === 404 || response.status === 409) {
960
+ const batchResponse = (await response.json()) as AssetBatchResponse;
961
+ throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, batchResponse);
962
+ }
963
+
964
+ const errorText = await response.text();
965
+ throw new Error(`Failed to execute asset batch: ${response.status} ${response.statusText} - ${errorText}`);
966
+ }
967
+
968
+ /**
969
+ * Retrieves multiple assets by their IDs in a single request.
970
+ *
971
+ * @param twinId - The twin ID
972
+ * @param ids - Array of asset IDs to retrieve (max 1000)
973
+ * @returns Batch response with summary and per-item results
974
+ * @throws {AssetValidationError} When request validation fails (400)
975
+ */
976
+ async batchGetAssets(twinId: string, ids: string[]): Promise<AssetBatchGetResponse> {
977
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch-get`, {
978
+ headers: { 'Content-Type': 'application/json' },
979
+ body: JSON.stringify({ ids })
980
+ });
981
+
982
+ if (response.ok) {
983
+ return (await response.json()) as AssetBatchGetResponse;
984
+ }
985
+
986
+ if (response.status === 400) {
987
+ const errorText = await response.text();
988
+ throw new AssetValidationError(`Asset batch-get validation failed: ${errorText}`);
989
+ }
990
+
991
+ const errorText = await response.text();
992
+ throw new Error(`Failed to batch-get assets: ${response.status} ${response.statusText} - ${errorText}`);
993
+ }
994
+
995
+ async listTwinFiles(
996
+ twinId: string,
997
+ options: TwinFileListOptions = {},
998
+ signal?: AbortSignal
999
+ ): Promise<PaginatedResponse<TwinFileGet>> {
1000
+ const { includeDeleted = false, checkedIn = false, version, page = undefined, limit = 100 } = options;
1001
+
1002
+ if (version !== undefined && !checkedIn) {
1003
+ throw new Error('The "version" option can only be used when "checkedIn" is true.');
1004
+ }
1005
+
1006
+ const params = new URLSearchParams();
1007
+ if (includeDeleted) params.set('includeDeleted', 'true');
1008
+ if (checkedIn) params.set('checkedIn', 'true');
1009
+ if (version !== undefined) params.set('version', version.toString());
1010
+ if (page !== undefined) params.set('page', page);
1011
+ if (limit !== 100) params.set('limit', limit.toString());
1012
+
1013
+ const url = params.toString()
1014
+ ? `${this._baseUrl}/${twinId}/files?${params}`
1015
+ : `${this._baseUrl}/${twinId}/files`;
1016
+
1017
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1018
+ headers: { 'Content-Type': 'application/json' },
1019
+ signal
1020
+ });
1021
+
1022
+ if (response.ok) {
1023
+ return (await response.json()) as PaginatedResponse<TwinFileGet>;
1024
+ }
1025
+
1026
+ if (response.status === 400) {
1027
+ const errorBody = await response.json();
1028
+ throw new TwinFileValidationError(`Twin file listing validation failed: ${errorBody.detail}`, errorBody);
1029
+ }
1030
+
1031
+ const errorText = await response.text();
1032
+ throw new Error(`Failed to list twin files: ${response.status} ${response.statusText} - ${errorText}`);
1033
+ }
1034
+
1035
+ async getTwinFile(twinId: string, fileId: string, signal?: AbortSignal): Promise<TwinFileGet> {
1036
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1037
+ headers: { 'Content-Type': 'application/json' },
1038
+ signal
1039
+ });
1040
+
1041
+ if (response.ok) {
1042
+ return (await response.json()) as TwinFileGet;
1043
+ }
1044
+
1045
+ if (response.status === 404) {
1046
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1047
+ }
1048
+
1049
+ if (response.status === 400) {
1050
+ const errorText = await response.text();
1051
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1052
+ }
1053
+
1054
+ const errorText = await response.text();
1055
+ throw new Error(`Failed to get twin file: ${response.status} ${response.statusText} - ${errorText}`);
1056
+ }
1057
+
1058
+ async createOrUpdateTwinFile(
1059
+ twinId: string,
1060
+ fileId: string,
1061
+ twinFile: TwinFilePut,
1062
+ signal?: AbortSignal
1063
+ ): Promise<TwinFileGet> {
1064
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1065
+ headers: { 'Content-Type': 'application/json' },
1066
+ body: JSON.stringify(twinFile),
1067
+ signal
1068
+ });
1069
+
1070
+ if (response.ok || response.status === 201) {
1071
+ return (await response.json()) as TwinFileGet;
1072
+ }
1073
+
1074
+ if (response.status === 400) {
1075
+ const errorBody = await response.json();
1076
+ throw new TwinFileValidationError(`Twin file validation failed`, errorBody);
1077
+ }
1078
+
1079
+ if (response.status === 409) {
1080
+ const conflictedFile = (await response.json()) as TwinFileGet;
1081
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1082
+ }
1083
+
1084
+ const errorText = await response.text();
1085
+ throw new Error(`Failed to create/update twin file: ${response.status} ${response.statusText} - ${errorText}`);
1086
+ }
1087
+
1088
+ async deleteTwinFile(
1089
+ twinId: string,
1090
+ fileId: string,
1091
+ deleteRequest: TwinFileDelete,
1092
+ signal?: AbortSignal
1093
+ ): Promise<TwinFileGet> {
1094
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1095
+ headers: { 'Content-Type': 'application/json' },
1096
+ body: JSON.stringify(deleteRequest),
1097
+ signal
1098
+ });
1099
+
1100
+ if (response.ok) {
1101
+ return (await response.json()) as TwinFileGet;
1102
+ }
1103
+
1104
+ if (response.status === 404) {
1105
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1106
+ }
1107
+
1108
+ if (response.status === 409) {
1109
+ const conflictedFile = (await response.json()) as TwinFileGet;
1110
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1111
+ }
1112
+
1113
+ if (response.status === 400) {
1114
+ const errorText = await response.text();
1115
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1116
+ }
1117
+
1118
+ const errorText = await response.text();
1119
+ throw new Error(`Failed to delete twin file: ${response.status} ${response.statusText} - ${errorText}`);
1120
+ }
1121
+
1122
+ async restoreTwinFile(twinId: string, fileId: string, etag: string, signal?: AbortSignal): Promise<TwinFileGet> {
1123
+ const response = await this._httpClient.fetch(
1124
+ HttpMethod.Put,
1125
+ `${this._baseUrl}/${twinId}/files/${fileId}/restore`,
1126
+ {
1127
+ headers: { 'Content-Type': 'application/json' },
1128
+ body: JSON.stringify({ etag }),
1129
+ signal
1130
+ }
1131
+ );
1132
+
1133
+ if (response.ok) {
1134
+ return (await response.json()) as TwinFileGet;
1135
+ }
1136
+
1137
+ if (response.status === 404) {
1138
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1139
+ }
1140
+
1141
+ if (response.status === 409) {
1142
+ const conflictedFile = (await response.json()) as TwinFileGet;
1143
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
1144
+ }
1145
+
1146
+ if (response.status === 400) {
1147
+ const errorText = await response.text();
1148
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
1149
+ }
1150
+
1151
+ const errorText = await response.text();
1152
+ throw new Error(`Failed to restore twin file: ${response.status} ${response.statusText} - ${errorText}`);
1153
+ }
1154
+
1155
+ async getTwinFileContent(twinId: string, fileId: string, signal?: AbortSignal): Promise<Blob> {
1156
+ const response = await this._httpClient.fetch(
1157
+ HttpMethod.Get,
1158
+ `${this._baseUrl}/${twinId}/files/${fileId}/content`,
1159
+ { signal }
1160
+ );
1161
+
1162
+ if (response.ok) {
1163
+ return await response.blob();
1164
+ }
1165
+
1166
+ if (response.status === 404) {
1167
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
1168
+ }
1169
+
1170
+ const errorText = await response.text();
1171
+ throw new Error(`Failed to get twin file content: ${response.status} ${response.statusText} - ${errorText}`);
1172
+ }
1173
+
1174
+ async getTwinFileVersion(
1175
+ twinId: string,
1176
+ fileId: string,
1177
+ version: number,
1178
+ signal?: AbortSignal
1179
+ ): Promise<TwinFileGet> {
1180
+ const response = await this._httpClient.fetch(
1181
+ HttpMethod.Get,
1182
+ `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}`,
1183
+ { signal }
1184
+ );
1185
+
1186
+ if (response.ok) {
1187
+ return (await response.json()) as TwinFileGet;
1188
+ }
1189
+
1190
+ if (response.status === 404) {
1191
+ throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} not found`);
1192
+ }
1193
+
1194
+ const errorText = await response.text();
1195
+ throw new Error(`Failed to get twin file version: ${response.status} ${response.statusText} - ${errorText}`);
1196
+ }
1197
+
1198
+ async listTwinFileVersions(
1199
+ twinId: string,
1200
+ fileId: string,
1201
+ options?: TwinFileVersionListOptions,
1202
+ signal?: AbortSignal
1203
+ ): Promise<PaginatedResponse<TwinFileGet>> {
1204
+ const params = new URLSearchParams();
1205
+ if (options?.includeDeleted !== undefined) params.append('includeDeleted', String(options.includeDeleted));
1206
+ if (options?.offset !== undefined) params.append('offset', String(options.offset));
1207
+ if (options?.limit !== undefined) params.append('limit', String(options.limit));
1208
+
1209
+ const response = await this._httpClient.fetch(
1210
+ HttpMethod.Get,
1211
+ `${this._baseUrl}/${twinId}/files/${fileId}/versions?${params.toString()}`,
1212
+ { signal }
1213
+ );
1214
+
1215
+ if (response.ok) {
1216
+ return (await response.json()) as PaginatedResponse<TwinFileGet>;
1217
+ }
1218
+
1219
+ if (response.status === 400) {
1220
+ const errorBody = await response.json();
1221
+ throw new Error(`Bad request: ${errorBody.detail || response.statusText}`);
1222
+ }
1223
+
1224
+ const errorText = await response.text();
1225
+ throw new Error(`Failed to list twin file versions: ${response.status} ${response.statusText} - ${errorText}`);
1226
+ }
1227
+
1228
+ async getTwinFileVersionContent(
1229
+ twinId: string,
1230
+ fileId: string,
1231
+ version: number,
1232
+ signal?: AbortSignal
1233
+ ): Promise<Blob> {
1234
+ const response = await this._httpClient.fetch(
1235
+ HttpMethod.Get,
1236
+ `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}/content`,
1237
+ { signal }
1238
+ );
1239
+
1240
+ if (response.ok) {
1241
+ return await response.blob();
1242
+ }
1243
+
1244
+ if (response.status === 404) {
1245
+ throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} content not found`);
1246
+ }
1247
+
1248
+ const errorText = await response.text();
1249
+ throw new Error(
1250
+ `Failed to get twin file version content: ${response.status} ${response.statusText} - ${errorText}`
1251
+ );
1252
+ }
1253
+
1254
+ async createOrUpdateTwinRaw(twinId: string, twin: unknown, signal?: AbortSignal): Promise<Response> {
1255
+ return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
1256
+ headers: { 'Content-Type': 'application/json' },
1257
+ body: JSON.stringify(twin),
1258
+ signal
1259
+ });
1260
+ }
1261
+
1262
+ async createOrUpdateTwinFileRaw(
1263
+ twinId: string,
1264
+ fileId: string,
1265
+ twinFile: unknown,
1266
+ signal?: AbortSignal
1267
+ ): Promise<Response> {
1268
+ return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
1269
+ headers: { 'Content-Type': 'application/json' },
1270
+ body: JSON.stringify(twinFile),
1271
+ signal
1272
+ });
1273
+ }
1274
+ }