@twinfinity/twin 0.0.0-feature.31596
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.
- package/README.md +277 -0
- package/dist/BuildInfo.d.ts +25 -0
- package/dist/BuildInfo.d.ts.map +1 -0
- package/dist/BuildInfo.js +15 -0
- package/dist/BuildInfo.js.map +1 -0
- package/dist/TwinClient.d.ts +823 -0
- package/dist/TwinClient.d.ts.map +1 -0
- package/dist/TwinClient.js +1487 -0
- package/dist/TwinClient.js.map +1 -0
- package/dist/errors.d.ts +289 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +357 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/Twin.d.ts +7 -0
- package/dist/types/Twin.d.ts.map +1 -0
- package/dist/types/Twin.js +6 -0
- package/dist/types/Twin.js.map +1 -0
- package/dist/types/TwinGeometryInfo.d.ts +7 -0
- package/dist/types/TwinGeometryInfo.d.ts.map +1 -0
- package/dist/types/TwinGeometryInfo.js +2 -0
- package/dist/types/TwinGeometryInfo.js.map +1 -0
- package/dist/types/asset-api.d.ts +66 -0
- package/dist/types/asset-api.d.ts.map +1 -0
- package/dist/types/asset-api.js +2 -0
- package/dist/types/asset-api.js.map +1 -0
- package/dist/types/asset-attachment-api.d.ts +51 -0
- package/dist/types/asset-attachment-api.d.ts.map +1 -0
- package/dist/types/asset-attachment-api.js +8 -0
- package/dist/types/asset-attachment-api.js.map +1 -0
- package/dist/types/asset-batch-api.d.ts +73 -0
- package/dist/types/asset-batch-api.d.ts.map +1 -0
- package/dist/types/asset-batch-api.js +2 -0
- package/dist/types/asset-batch-api.js.map +1 -0
- package/dist/types/asset-types-api.d.ts +76 -0
- package/dist/types/asset-types-api.d.ts.map +1 -0
- package/dist/types/asset-types-api.js +5 -0
- package/dist/types/asset-types-api.js.map +1 -0
- package/dist/types/common.d.ts +10 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +2 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/generated.d.ts +458 -0
- package/dist/types/generated.d.ts.map +1 -0
- package/dist/types/generated.js +2 -0
- package/dist/types/generated.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/twin-api.d.ts +141 -0
- package/dist/types/twin-api.d.ts.map +1 -0
- package/dist/types/twin-api.js +2 -0
- package/dist/types/twin-api.js.map +1 -0
- package/dist/types/twin-file-api.d.ts +61 -0
- package/dist/types/twin-file-api.d.ts.map +1 -0
- package/dist/types/twin-file-api.js +2 -0
- package/dist/types/twin-file-api.js.map +1 -0
- package/dist/types/twin-permissions-api.d.ts +240 -0
- package/dist/types/twin-permissions-api.d.ts.map +1 -0
- package/dist/types/twin-permissions-api.js +2 -0
- package/dist/types/twin-permissions-api.js.map +1 -0
- package/docs/.nojekyll +1 -0
- package/docs/assets/hierarchy.js +1 -0
- package/docs/assets/highlight.css +99 -0
- package/docs/assets/icons.js +18 -0
- package/docs/assets/icons.svg +1 -0
- package/docs/assets/main.js +60 -0
- package/docs/assets/navigation.js +1 -0
- package/docs/assets/search.js +1 -0
- package/docs/assets/style.css +1633 -0
- package/docs/classes/AssetAttachmentConflictError.html +3 -0
- package/docs/classes/AssetAttachmentNotFoundError.html +2 -0
- package/docs/classes/AssetBatchError.html +3 -0
- package/docs/classes/AssetCatalogTooLargeError.html +16 -0
- package/docs/classes/AssetCategoryConflictError.html +3 -0
- package/docs/classes/AssetCategoryForbiddenError.html +2 -0
- package/docs/classes/AssetCategoryNotFoundError.html +2 -0
- package/docs/classes/AssetCategoryValidationError.html +3 -0
- package/docs/classes/AssetConflictError.html +3 -0
- package/docs/classes/AssetForbiddenError.html +2 -0
- package/docs/classes/AssetNotFoundError.html +2 -0
- package/docs/classes/AssetPermissionsConflictError.html +16 -0
- package/docs/classes/AssetTypeConflictError.html +3 -0
- package/docs/classes/AssetTypeForbiddenError.html +2 -0
- package/docs/classes/AssetTypeNotFoundError.html +2 -0
- package/docs/classes/AssetTypeValidationError.html +3 -0
- package/docs/classes/AssetValidationError.html +3 -0
- package/docs/classes/ProcessorNotFoundError.html +2 -0
- package/docs/classes/TwinClient.html +409 -0
- package/docs/classes/TwinConflictError.html +3 -0
- package/docs/classes/TwinFileConflictError.html +3 -0
- package/docs/classes/TwinFileNotFoundError.html +2 -0
- package/docs/classes/TwinFileValidationError.html +3 -0
- package/docs/classes/TwinNotFoundError.html +4 -0
- package/docs/classes/TwinPermissionsConflictError.html +19 -0
- package/docs/classes/TwinPermissionsForbiddenError.html +12 -0
- package/docs/classes/TwinPrincipalNotFoundError.html +5 -0
- package/docs/classes/TwinTemporarilyUnavailableError.html +10 -0
- package/docs/classes/TwinUnauthenticatedError.html +6 -0
- package/docs/classes/TwinValidationError.html +3 -0
- package/docs/functions/isAssetCatalogTooLargeErrorBody.html +6 -0
- package/docs/functions/readErrorBody.html +10 -0
- package/docs/hierarchy.html +1 -0
- package/docs/index.html +168 -0
- package/docs/interfaces/ApiCreateUploadSessionResponse.html +7 -0
- package/docs/interfaces/AssetAttachmentPut.html +6 -0
- package/docs/interfaces/AssetBatchDeleteOperation.html +4 -0
- package/docs/interfaces/AssetBatchGetRequest.html +2 -0
- package/docs/interfaces/AssetBatchGetResponse.html +3 -0
- package/docs/interfaces/AssetBatchRequest.html +2 -0
- package/docs/interfaces/AssetBatchResponse.html +3 -0
- package/docs/interfaces/AssetBatchRestoreOperation.html +4 -0
- package/docs/interfaces/AssetBatchUpsertItem.html +12 -0
- package/docs/interfaces/AssetBatchUpsertOperation.html +4 -0
- package/docs/interfaces/AssetCatalogCategory.html +7 -0
- package/docs/interfaces/AssetCatalogResponse.html +6 -0
- package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +15 -0
- package/docs/interfaces/AssetCategoryDelete.html +2 -0
- package/docs/interfaces/AssetCategoryGet.html +14 -0
- package/docs/interfaces/AssetCategoryPut.html +7 -0
- package/docs/interfaces/AssetGet.html +18 -0
- package/docs/interfaces/AssetGroupGet.html +9 -0
- package/docs/interfaces/AssetGroupsGet.html +3 -0
- package/docs/interfaces/AssetListItem.html +14 -0
- package/docs/interfaces/AssetPermissionsConflictErrorBody.html +12 -0
- package/docs/interfaces/AssetPermissionsPut.html +9 -0
- package/docs/interfaces/AssetPosition.html +4 -0
- package/docs/interfaces/AssetPost.html +12 -0
- package/docs/interfaces/AssetPrincipalPermissionsGet.html +23 -0
- package/docs/interfaces/AssetPut.html +12 -0
- package/docs/interfaces/AssetTypeDelete.html +2 -0
- package/docs/interfaces/AssetTypeGet.html +16 -0
- package/docs/interfaces/AssetTypePut.html +9 -0
- package/docs/interfaces/AttachmentGet.html +20 -0
- package/docs/interfaces/AttachmentPost.html +4 -0
- package/docs/interfaces/BatchItemError.html +4 -0
- package/docs/interfaces/BatchItemResult.html +6 -0
- package/docs/interfaces/BatchSummary.html +4 -0
- package/docs/interfaces/Coordinates.html +6 -0
- package/docs/interfaces/Diagnostics.html +4 -0
- package/docs/interfaces/DiagnosticsSummary.html +4 -0
- package/docs/interfaces/ElementQuantities.html +3 -0
- package/docs/interfaces/EntityCollection.html +15 -0
- package/docs/interfaces/EntityRepresentation.html +12 -0
- package/docs/interfaces/Facility.html +22 -0
- package/docs/interfaces/FacilityPart.html +18 -0
- package/docs/interfaces/FilePatch.html +7 -0
- package/docs/interfaces/FilePatchSet.html +5 -0
- package/docs/interfaces/GenericDiagnosticEntry.html +4 -0
- package/docs/interfaces/ITwinClient.html +404 -0
- package/docs/interfaces/IfcDiagnosticEntry.html +5 -0
- package/docs/interfaces/IfcDiagnosticNode.html +6 -0
- package/docs/interfaces/IfcDiagnosticNodeData.html +6 -0
- package/docs/interfaces/InheritedPermissions.html +8 -0
- package/docs/interfaces/ListTwinItem.html +30 -0
- package/docs/interfaces/Location.html +13 -0
- package/docs/interfaces/MyPermissions.html +7 -0
- package/docs/interfaces/MyTwinPermissions.html +5 -0
- package/docs/interfaces/PaginatedResponse.html +3 -0
- package/docs/interfaces/Pagination.html +4 -0
- package/docs/interfaces/Property.html +4 -0
- package/docs/interfaces/PropertySet.html +6 -0
- package/docs/interfaces/SimpleQuantity.html +6 -0
- package/docs/interfaces/SinglePropertyValue.html +5 -0
- package/docs/interfaces/Site.html +27 -0
- package/docs/interfaces/Space.html +16 -0
- package/docs/interfaces/TwinComputedPropertySetsResponse.html +4 -0
- package/docs/interfaces/TwinContentResponse.html +6 -0
- package/docs/interfaces/TwinDelete.html +2 -0
- package/docs/interfaces/TwinDiagnosticEntry.html +5 -0
- package/docs/interfaces/TwinDiagnosticNode.html +4 -0
- package/docs/interfaces/TwinDiagnosticNodeData.html +9 -0
- package/docs/interfaces/TwinDiagnosticsResponse.html +5 -0
- package/docs/interfaces/TwinErrorBody.html +4 -0
- package/docs/interfaces/TwinFileDelete.html +2 -0
- package/docs/interfaces/TwinFileGet.html +15 -0
- package/docs/interfaces/TwinFileListOptions.html +6 -0
- package/docs/interfaces/TwinFilePut.html +8 -0
- package/docs/interfaces/TwinFileReference.html +4 -0
- package/docs/interfaces/TwinFileRestore.html +2 -0
- package/docs/interfaces/TwinFileSource.html +4 -0
- package/docs/interfaces/TwinFileTransform.html +11 -0
- package/docs/interfaces/TwinFileVersionListOptions.html +4 -0
- package/docs/interfaces/TwinGet.html +48 -0
- package/docs/interfaces/TwinGroupGet.html +10 -0
- package/docs/interfaces/TwinGroupsGet.html +3 -0
- package/docs/interfaces/TwinPermissionsConflictErrorBody.html +8 -0
- package/docs/interfaces/TwinPermissionsPut.html +11 -0
- package/docs/interfaces/TwinPrincipalPermissionsGet.html +34 -0
- package/docs/interfaces/TwinProcessorEvent.html +7 -0
- package/docs/interfaces/TwinProcessorResponse.html +5 -0
- package/docs/interfaces/TwinPut.html +21 -0
- package/docs/interfaces/TwinReference.html +4 -0
- package/docs/interfaces/TwinSpecification.html +18 -0
- package/docs/interfaces/TwinVersionContentResponse.html +7 -0
- package/docs/interfaces/UploadStatusGet.html +3 -0
- package/docs/media/twin-management.md +399 -0
- package/docs/modules.html +1 -0
- package/docs/types/AssetBatchGetStatus.html +1 -0
- package/docs/types/AssetBatchOperation.html +1 -0
- package/docs/types/AssetBatchOperationStatus.html +1 -0
- package/docs/types/DiagnosticCategory.html +2 -0
- package/docs/types/DiagnosticLevel.html +2 -0
- package/docs/types/EpsgType.html +1 -0
- package/docs/types/FacilityClass.html +3 -0
- package/docs/types/FacilityPartClass.html +3 -0
- package/docs/types/IfcDiagnosticCode.html +1 -0
- package/docs/types/Op.html +2 -0
- package/docs/types/SinglePropertyValueType.html +3 -0
- package/docs/types/SingleQuantityValueType.html +3 -0
- package/docs/types/SiteType.html +3 -0
- package/docs/types/SpaceType.html +2 -0
- package/docs/types/TwinBoundCapabilityName.html +8 -0
- package/docs/types/TwinCapability.html +12 -0
- package/docs/types/TwinCapabilityName.html +8 -0
- package/docs/types/TwinDiagnosticCode.html +1 -0
- package/docs/types/TwinErrorCause.html +8 -0
- package/docs/types/TwinErrorItemKind.html +4 -0
- package/docs/types/TwinPermissionsConflictCause.html +6 -0
- package/docs/types/TwinScopedCapability.html +9 -0
- package/docs/types/TwinScopedCapabilityName.html +8 -0
- package/docs/types/UploadStatus.html +1 -0
- package/package.json +52 -0
- package/src/BuildInfo.ts +41 -0
- package/src/TwinClient.ts +3113 -0
- package/src/errors.ts +567 -0
- package/src/index.ts +3 -0
- package/src/tsconfig.json +13 -0
- package/src/types/Twin.ts +7 -0
- package/src/types/TwinGeometryInfo.ts +7 -0
- package/src/types/asset-api.ts +69 -0
- package/src/types/asset-attachment-api.ts +56 -0
- package/src/types/asset-batch-api.ts +90 -0
- package/src/types/asset-types-api.ts +86 -0
- package/src/types/common.ts +10 -0
- package/src/types/generated.ts +655 -0
- package/src/types/index.ts +9 -0
- package/src/types/twin-api.ts +153 -0
- package/src/types/twin-file-api.ts +67 -0
- package/src/types/twin-permissions-api.ts +257 -0
|
@@ -0,0 +1,1487 @@
|
|
|
1
|
+
import { HttpMethod } from '@twinfinity/authentication';
|
|
2
|
+
import { readT8ClientModelWithFactories, readT8ClientModelWithGeometry, readT8GIModelWithFactories, T8EntityType, T8GIMeshDescriptor, T8GIStyle, T8GIRegion, T8Product, T8Project, T8Unit } from '@twinfinity/t8-client-model';
|
|
3
|
+
import { TwinValidationError, TwinConflictError, TwinNotFoundError, ProcessorNotFoundError, AssetValidationError, AssetForbiddenError, AssetConflictError, AssetNotFoundError, AssetAttachmentConflictError, AssetAttachmentNotFoundError, AssetBatchError, TwinFileValidationError, TwinFileConflictError, TwinFileNotFoundError, AssetCategoryValidationError, AssetCategoryConflictError, AssetCategoryNotFoundError, AssetCategoryForbiddenError, AssetTypeValidationError, AssetTypeConflictError, AssetTypeNotFoundError, AssetTypeForbiddenError, AssetCatalogTooLargeError, isAssetCatalogTooLargeErrorBody, TwinUnauthenticatedError, TwinPermissionsConflictError, TwinPermissionsForbiddenError, TwinPrincipalNotFoundError, TwinTemporarilyUnavailableError, AssetPermissionsConflictError, readErrorBody } from './errors';
|
|
4
|
+
/**
|
|
5
|
+
* The `Retry-After` of a 503, in seconds, when the response carried a usable one. The Twin service
|
|
6
|
+
* sends a delta-seconds value; an HTTP-date is permitted by RFC 7231 and simply yields nothing here
|
|
7
|
+
* rather than a nonsense delay.
|
|
8
|
+
*/
|
|
9
|
+
function retryAfterSeconds(response) {
|
|
10
|
+
const header = response.headers?.get('Retry-After');
|
|
11
|
+
if (header === null || header === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const seconds = Number(header);
|
|
14
|
+
return Number.isInteger(seconds) && seconds >= 0 ? seconds : undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The batch response body carried by a failed batch, or `undefined` when the body is not one.
|
|
18
|
+
*
|
|
19
|
+
* A rolled-back batch answers with per-item results under the status of its *last failing item* --
|
|
20
|
+
* so the same endpoint returns a batch body under 400, 404 or 409, while a whole-request rejection
|
|
21
|
+
* (an empty batch, duplicate ids, a body/URL twin mismatch, oversized metadata) returns problem
|
|
22
|
+
* details under 400 as well. Status alone cannot tell those apart, and branching on it reported
|
|
23
|
+
* every per-item 400 -- a cross-twin id, a deleted asset, metadata failing its schema -- as a
|
|
24
|
+
* validation failure with no results for the caller to inspect.
|
|
25
|
+
*/
|
|
26
|
+
async function readBatchResponse(response) {
|
|
27
|
+
// Parse a clone: json() consumes the stream, and a body that turns out not to be a batch
|
|
28
|
+
// response still has to be readable as problem details or as text by the caller. Without
|
|
29
|
+
// clone -- some test doubles -- the probe is skipped rather than risk consuming the body.
|
|
30
|
+
if (typeof response.clone !== 'function')
|
|
31
|
+
return undefined;
|
|
32
|
+
try {
|
|
33
|
+
const parsed = (await response.clone().json());
|
|
34
|
+
return Array.isArray(parsed?.results) ? parsed : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class TwinClient {
|
|
41
|
+
_httpClient;
|
|
42
|
+
_baseUrl;
|
|
43
|
+
_assetTypeCatalogUrl;
|
|
44
|
+
constructor(baseUrl, _httpClient) {
|
|
45
|
+
this._httpClient = _httpClient;
|
|
46
|
+
const base = String(baseUrl).replace(/\/+$/, '');
|
|
47
|
+
this._baseUrl = `${base}/_ps/twins`;
|
|
48
|
+
this._assetTypeCatalogUrl = `${base}/_ps/asset-type-catalog`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Rooted twins base URL — typically `<host>/sites/<container>/_ps/twins`.
|
|
52
|
+
* Exposed so callers that want to build content URLs in the same auth
|
|
53
|
+
* scope as the metadata calls can derive them from here. The bare
|
|
54
|
+
* `<host>/_ps/twins/...` variant is not necessarily an authorized route
|
|
55
|
+
* on all deployments.
|
|
56
|
+
*/
|
|
57
|
+
get baseUrl() {
|
|
58
|
+
return this._baseUrl;
|
|
59
|
+
}
|
|
60
|
+
async listTwins(optionsOrRelatedTo, pageOrSignal, positionalLimit, positionalSignal) {
|
|
61
|
+
let relatedTo;
|
|
62
|
+
let includeDeleted = false;
|
|
63
|
+
let page;
|
|
64
|
+
let limit = 100;
|
|
65
|
+
let signal;
|
|
66
|
+
if (typeof optionsOrRelatedTo === 'string') {
|
|
67
|
+
relatedTo = optionsOrRelatedTo;
|
|
68
|
+
page = pageOrSignal;
|
|
69
|
+
limit = positionalLimit ?? 100;
|
|
70
|
+
signal = positionalSignal;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
({ relatedTo, includeDeleted = false, page = undefined, limit = 100 } = optionsOrRelatedTo ?? {});
|
|
74
|
+
signal = pageOrSignal;
|
|
75
|
+
}
|
|
76
|
+
const params = new URLSearchParams();
|
|
77
|
+
if (relatedTo)
|
|
78
|
+
params.set('relatedTo', relatedTo);
|
|
79
|
+
if (page !== undefined)
|
|
80
|
+
params.set('page', page);
|
|
81
|
+
if (limit !== 100)
|
|
82
|
+
params.set('limit', limit.toString());
|
|
83
|
+
if (includeDeleted)
|
|
84
|
+
params.set('includeDeleted', 'true');
|
|
85
|
+
const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
|
|
86
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
87
|
+
headers: { Accept: 'application/json' },
|
|
88
|
+
signal
|
|
89
|
+
});
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
return (await response.json());
|
|
92
|
+
}
|
|
93
|
+
if (response.status === 400) {
|
|
94
|
+
const errorBody = await readErrorBody(response);
|
|
95
|
+
throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
|
|
96
|
+
}
|
|
97
|
+
const errorText = await response.text();
|
|
98
|
+
throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
|
|
99
|
+
}
|
|
100
|
+
async listAllTwins(optionsOrRelatedTo, signalArg) {
|
|
101
|
+
let relatedTo;
|
|
102
|
+
let includeDeleted = false;
|
|
103
|
+
let signal;
|
|
104
|
+
if (typeof optionsOrRelatedTo === 'string') {
|
|
105
|
+
relatedTo = optionsOrRelatedTo;
|
|
106
|
+
signal = signalArg;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
({ relatedTo, includeDeleted = false } = optionsOrRelatedTo ?? {});
|
|
110
|
+
signal = signalArg;
|
|
111
|
+
}
|
|
112
|
+
const allTwins = [];
|
|
113
|
+
let page = undefined;
|
|
114
|
+
const limit = 100;
|
|
115
|
+
const maxPages = 1000;
|
|
116
|
+
for (let pageCount = 0; pageCount < maxPages; pageCount++) {
|
|
117
|
+
const response = await this.listTwins({ relatedTo, page, limit, includeDeleted }, signal);
|
|
118
|
+
allTwins.push(...response.data);
|
|
119
|
+
if (response.data.length < limit) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
page = response.pagination.nextPage;
|
|
123
|
+
}
|
|
124
|
+
return allTwins;
|
|
125
|
+
}
|
|
126
|
+
async getTwin(twin, signal) {
|
|
127
|
+
const twinId = typeof twin === 'string' ? twin : twin.id;
|
|
128
|
+
const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
|
|
129
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}${specifiedVersion !== undefined ? `/versions/${specifiedVersion}` : ''}`, {
|
|
130
|
+
headers: { Accept: 'application/json' },
|
|
131
|
+
signal
|
|
132
|
+
});
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
return (await response.json());
|
|
135
|
+
}
|
|
136
|
+
if (response.status === 404) {
|
|
137
|
+
throw new TwinNotFoundError(`Twin ${twinId} not found`);
|
|
138
|
+
}
|
|
139
|
+
const errorText = await response.text();
|
|
140
|
+
throw new Error(`Failed to get twin: ${response.status} ${response.statusText} - ${errorText}`);
|
|
141
|
+
}
|
|
142
|
+
async listTwinVersions(id, options, signal) {
|
|
143
|
+
const params = new URLSearchParams();
|
|
144
|
+
if (options?.page !== undefined)
|
|
145
|
+
params.set('page', options.page);
|
|
146
|
+
if (options?.limit !== undefined && options.limit !== 100)
|
|
147
|
+
params.set('limit', options.limit.toString());
|
|
148
|
+
if (options?.includeDeleted)
|
|
149
|
+
params.set('includeDeleted', 'true');
|
|
150
|
+
const url = `${this._baseUrl}/${id}/versions${params.toString() ? `?${params}` : ''}`;
|
|
151
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
152
|
+
headers: { Accept: 'application/json' },
|
|
153
|
+
signal
|
|
154
|
+
});
|
|
155
|
+
if (response.ok) {
|
|
156
|
+
return (await response.json());
|
|
157
|
+
}
|
|
158
|
+
if (response.status === 400) {
|
|
159
|
+
const errorBody = await readErrorBody(response);
|
|
160
|
+
throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
|
|
161
|
+
}
|
|
162
|
+
const errorText = await response.text();
|
|
163
|
+
throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
|
|
164
|
+
}
|
|
165
|
+
async downloadTwinContent(twinId, twinVersion, content, signal) {
|
|
166
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents/${content.id}/content`, { signal });
|
|
167
|
+
if (response.ok) {
|
|
168
|
+
const blob = await response.blob();
|
|
169
|
+
return await blob.arrayBuffer();
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Failed to download twin content: ${response.status} ${response.statusText}`);
|
|
172
|
+
}
|
|
173
|
+
async loadTwinVersion(twinId, twinVersion, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal) {
|
|
174
|
+
const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
|
|
175
|
+
const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
|
|
176
|
+
if (t8cmItem === undefined) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
|
|
180
|
+
const data = await Promise.all([this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal)].concat(t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal))));
|
|
181
|
+
return readT8ClientModelWithFactories(data, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct);
|
|
182
|
+
}
|
|
183
|
+
loadTwin(twin, signal) {
|
|
184
|
+
return this.loadTwinWithFactories(twin, (T8Project.create), (T8Product.create), T8EntityType.create, T8Unit.create, T8Project.connect, T8Product.connect, signal);
|
|
185
|
+
}
|
|
186
|
+
async loadTwinWithFactories(twin, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal) {
|
|
187
|
+
let page = undefined;
|
|
188
|
+
const twinId = typeof twin === 'string' ? twin : twin.id;
|
|
189
|
+
const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
|
|
190
|
+
if (specifiedVersion !== undefined) {
|
|
191
|
+
return {
|
|
192
|
+
id: twinId,
|
|
193
|
+
version: specifiedVersion,
|
|
194
|
+
model: await this.loadTwinVersion(twinId, specifiedVersion, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const maxPages = 100;
|
|
198
|
+
for (let pageCount = 0; pageCount < maxPages; pageCount++) {
|
|
199
|
+
const versions = await this.listTwinVersions(twinId, { page }, signal);
|
|
200
|
+
if (versions.data.length === 0) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
for (const versionItem of versions.data) {
|
|
204
|
+
const loadedModel = await this.loadTwinVersion(twinId, versionItem.version, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal);
|
|
205
|
+
if (loadedModel !== undefined) {
|
|
206
|
+
return {
|
|
207
|
+
id: twinId,
|
|
208
|
+
version: versionItem.version,
|
|
209
|
+
model: loadedModel
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
page = versions.pagination.nextPage;
|
|
214
|
+
}
|
|
215
|
+
throw new Error(`Twin ${twinId} does not have a client model`);
|
|
216
|
+
}
|
|
217
|
+
async loadGeometryInfoVersion(twinId, twinVersion, meshDescriptorFactory, styleFactory, regionFactory, signal) {
|
|
218
|
+
const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
|
|
219
|
+
const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
|
|
220
|
+
if (t8giItem === undefined) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
const data = await this.downloadTwinContent(twinId, twinVersion, t8giItem, signal);
|
|
224
|
+
return readT8GIModelWithFactories(data, meshDescriptorFactory, styleFactory, regionFactory);
|
|
225
|
+
}
|
|
226
|
+
loadGeometryInfo(twin, signal) {
|
|
227
|
+
return this.loadGeometryInfoWithFactories(twin, T8GIMeshDescriptor.create, T8GIStyle.create, T8GIRegion.create, signal);
|
|
228
|
+
}
|
|
229
|
+
async loadGeometryInfoWithFactories(twin, meshDescriptorFactory, styleFactory, regionFactory, signal) {
|
|
230
|
+
let page = undefined;
|
|
231
|
+
const twinId = typeof twin === 'string' ? twin : twin.id;
|
|
232
|
+
const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
|
|
233
|
+
if (specifiedVersion !== undefined) {
|
|
234
|
+
return {
|
|
235
|
+
id: twinId,
|
|
236
|
+
version: specifiedVersion,
|
|
237
|
+
model: await this.loadGeometryInfoVersion(twinId, specifiedVersion, meshDescriptorFactory, styleFactory, regionFactory, signal)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const maxPages = 100;
|
|
241
|
+
for (let pageCount = 0; pageCount < maxPages; pageCount++) {
|
|
242
|
+
const versions = await this.listTwinVersions(twinId, { page }, signal);
|
|
243
|
+
if (versions.data.length === 0) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
for (const versionItem of versions.data) {
|
|
247
|
+
const model = await this.loadGeometryInfoVersion(twinId, versionItem.version, meshDescriptorFactory, styleFactory, regionFactory, signal);
|
|
248
|
+
if (model !== undefined) {
|
|
249
|
+
return {
|
|
250
|
+
id: twinId,
|
|
251
|
+
version: versionItem.version,
|
|
252
|
+
model
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
page = versions.pagination.nextPage;
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Twin ${twinId} does not have geometry info`);
|
|
259
|
+
}
|
|
260
|
+
async loadTwinWithGeometryVersion(twinId, twinVersion, signal) {
|
|
261
|
+
const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
|
|
262
|
+
const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
|
|
263
|
+
const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
|
|
264
|
+
if (t8cmItem === undefined || t8giItem === undefined) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
|
|
268
|
+
const t8gdItem = twinContent.content.find((item) => item.format === 't8gd');
|
|
269
|
+
const downloads = [
|
|
270
|
+
this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal),
|
|
271
|
+
...t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal)),
|
|
272
|
+
this.downloadTwinContent(twinId, twinVersion, t8giItem, signal),
|
|
273
|
+
...(t8gdItem ? [this.downloadTwinContent(twinId, twinVersion, t8gdItem, signal)] : [])
|
|
274
|
+
];
|
|
275
|
+
const allData = await Promise.all(downloads);
|
|
276
|
+
const t8cmCount = 1 + t8cmExtensions.length;
|
|
277
|
+
const t8cmData = allData.slice(0, t8cmCount);
|
|
278
|
+
const t8giData = allData[t8cmCount];
|
|
279
|
+
const t8gdData = t8gdItem ? allData[t8cmCount + 1] : undefined;
|
|
280
|
+
return readT8ClientModelWithGeometry(t8cmData, t8giData, t8gdData);
|
|
281
|
+
}
|
|
282
|
+
async loadTwinWithGeometry(twin, signal) {
|
|
283
|
+
let page = undefined;
|
|
284
|
+
const twinId = typeof twin === 'string' ? twin : twin.id;
|
|
285
|
+
const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
|
|
286
|
+
if (specifiedVersion !== undefined) {
|
|
287
|
+
return {
|
|
288
|
+
id: twinId,
|
|
289
|
+
version: specifiedVersion,
|
|
290
|
+
model: await this.loadTwinWithGeometryVersion(twinId, specifiedVersion, signal)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const maxPages = 100;
|
|
294
|
+
for (let pageCount = 0; pageCount < maxPages; pageCount++) {
|
|
295
|
+
const versions = await this.listTwinVersions(twinId, { page }, signal);
|
|
296
|
+
if (versions.data.length === 0) {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
for (const versionItem of versions.data) {
|
|
300
|
+
const model = await this.loadTwinWithGeometryVersion(twinId, versionItem.version, signal);
|
|
301
|
+
if (model !== undefined) {
|
|
302
|
+
return {
|
|
303
|
+
id: twinId,
|
|
304
|
+
version: versionItem.version,
|
|
305
|
+
model
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
page = versions.pagination.nextPage;
|
|
310
|
+
}
|
|
311
|
+
throw new Error(`Twin ${twinId} does not have a client model with geometry`);
|
|
312
|
+
}
|
|
313
|
+
async createOrUpdateTwin(twinId, twin, signal) {
|
|
314
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify(twin),
|
|
317
|
+
signal
|
|
318
|
+
});
|
|
319
|
+
if (response.ok) {
|
|
320
|
+
return (await response.json());
|
|
321
|
+
}
|
|
322
|
+
if (response.status === 400) {
|
|
323
|
+
const errorBody = await readErrorBody(response);
|
|
324
|
+
throw new TwinValidationError(`Twin validation failed: ${errorBody.detail}`, errorBody);
|
|
325
|
+
}
|
|
326
|
+
if (response.status === 409) {
|
|
327
|
+
const conflictedTwin = (await response.json());
|
|
328
|
+
throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
|
|
329
|
+
}
|
|
330
|
+
const errorText = await response.text();
|
|
331
|
+
throw new Error(`Failed to create/update twin: ${response.status} ${response.statusText} - ${errorText}`);
|
|
332
|
+
}
|
|
333
|
+
async updateTwin(twinId, twin, signal) {
|
|
334
|
+
return this.createOrUpdateTwin(twinId, twin, signal);
|
|
335
|
+
}
|
|
336
|
+
async deleteTwin(twinId, etag, signal) {
|
|
337
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}`, {
|
|
338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
339
|
+
body: JSON.stringify({ etag }),
|
|
340
|
+
signal
|
|
341
|
+
});
|
|
342
|
+
if (response.ok) {
|
|
343
|
+
return (await response.json());
|
|
344
|
+
}
|
|
345
|
+
if (response.status === 404) {
|
|
346
|
+
throw new TwinNotFoundError(`Twin ${twinId} not found`);
|
|
347
|
+
}
|
|
348
|
+
if (response.status === 409) {
|
|
349
|
+
const conflictedTwin = (await response.json());
|
|
350
|
+
throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
|
|
351
|
+
}
|
|
352
|
+
const errorText = await response.text();
|
|
353
|
+
throw new Error(`Failed to delete twin: ${response.status} ${response.statusText} - ${errorText}`);
|
|
354
|
+
}
|
|
355
|
+
async restoreTwin(twinId, etag, signal) {
|
|
356
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/restore`, {
|
|
357
|
+
headers: { 'Content-Type': 'application/json' },
|
|
358
|
+
body: JSON.stringify({ etag }),
|
|
359
|
+
signal
|
|
360
|
+
});
|
|
361
|
+
if (response.ok) {
|
|
362
|
+
return (await response.json());
|
|
363
|
+
}
|
|
364
|
+
if (response.status === 404) {
|
|
365
|
+
throw new TwinNotFoundError(`Twin ${twinId} not found`);
|
|
366
|
+
}
|
|
367
|
+
if (response.status === 409) {
|
|
368
|
+
const conflictedTwin = (await response.json());
|
|
369
|
+
throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
|
|
370
|
+
}
|
|
371
|
+
const errorText = await response.text();
|
|
372
|
+
throw new Error(`Failed to restore twin: ${response.status} ${response.statusText} - ${errorText}`);
|
|
373
|
+
}
|
|
374
|
+
async getTwinVersionContent(twinId, twinVersion, signal) {
|
|
375
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`, { headers: { Accept: 'application/json' }, signal });
|
|
376
|
+
if (response.ok) {
|
|
377
|
+
return (await response.json());
|
|
378
|
+
}
|
|
379
|
+
if (response.status === 404) {
|
|
380
|
+
throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
|
|
381
|
+
}
|
|
382
|
+
const errorText = await response.text();
|
|
383
|
+
throw new Error(`Failed to get twin version content: ${response.status} ${response.statusText} - ${errorText}`);
|
|
384
|
+
}
|
|
385
|
+
async getTwinContent(twinId, signal) {
|
|
386
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/contents`, {
|
|
387
|
+
headers: { Accept: 'application/json' },
|
|
388
|
+
signal
|
|
389
|
+
});
|
|
390
|
+
if (response.ok) {
|
|
391
|
+
return (await response.json());
|
|
392
|
+
}
|
|
393
|
+
if (response.status === 404) {
|
|
394
|
+
throw new TwinNotFoundError(`Twin ${twinId} not found`);
|
|
395
|
+
}
|
|
396
|
+
const errorText = await response.text();
|
|
397
|
+
throw new Error(`Failed to get twin content: ${response.status} ${response.statusText} - ${errorText}`);
|
|
398
|
+
}
|
|
399
|
+
async getTwinDiagnostics(twinId, signal) {
|
|
400
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/diagnostics`, {
|
|
401
|
+
headers: { Accept: 'application/json' },
|
|
402
|
+
signal
|
|
403
|
+
});
|
|
404
|
+
if (response.ok) {
|
|
405
|
+
return (await response.json());
|
|
406
|
+
}
|
|
407
|
+
if (response.status === 404) {
|
|
408
|
+
throw new TwinNotFoundError(`Twin ${twinId} not found`);
|
|
409
|
+
}
|
|
410
|
+
const errorText = await response.text();
|
|
411
|
+
throw new Error(`Failed to get twin diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
|
|
412
|
+
}
|
|
413
|
+
async getTwinVersionDiagnostics(twinId, twinVersion, signal) {
|
|
414
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`, { headers: { Accept: 'application/json' }, signal });
|
|
415
|
+
if (response.ok) {
|
|
416
|
+
return (await response.json());
|
|
417
|
+
}
|
|
418
|
+
if (response.status === 404) {
|
|
419
|
+
throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
|
|
420
|
+
}
|
|
421
|
+
const errorText = await response.text();
|
|
422
|
+
throw new Error(`Failed to get twin version diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
|
|
423
|
+
}
|
|
424
|
+
async getTwinVersionEvents(twinId, twinVersion, offset = 0, limit = 100, signal) {
|
|
425
|
+
const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/events?offset=${offset}&limit=${limit}`;
|
|
426
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
427
|
+
headers: { Accept: 'application/json' },
|
|
428
|
+
signal
|
|
429
|
+
});
|
|
430
|
+
if (response.ok) {
|
|
431
|
+
return (await response.json());
|
|
432
|
+
}
|
|
433
|
+
if (response.status === 400) {
|
|
434
|
+
const errorBody = await readErrorBody(response);
|
|
435
|
+
throw new TwinValidationError(`Twin events validation failed: ${errorBody.detail}`, errorBody);
|
|
436
|
+
}
|
|
437
|
+
if (response.status === 404) {
|
|
438
|
+
throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
|
|
439
|
+
}
|
|
440
|
+
const errorText = await response.text();
|
|
441
|
+
throw new Error(`Failed to get twin version events: ${response.status} ${response.statusText} - ${errorText}`);
|
|
442
|
+
}
|
|
443
|
+
async getProcessorEvents(twinId, twinVersion, processorId, offset = 0, limit = 100, signal) {
|
|
444
|
+
const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/processors/${processorId}/events?offset=${offset}&limit=${limit}`;
|
|
445
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
446
|
+
headers: { Accept: 'application/json' },
|
|
447
|
+
signal
|
|
448
|
+
});
|
|
449
|
+
if (response.ok) {
|
|
450
|
+
return (await response.json());
|
|
451
|
+
}
|
|
452
|
+
if (response.status === 400) {
|
|
453
|
+
const errorBody = await readErrorBody(response);
|
|
454
|
+
throw new TwinValidationError(`Processor events validation failed: ${errorBody.detail}`, errorBody);
|
|
455
|
+
}
|
|
456
|
+
if (response.status === 404) {
|
|
457
|
+
throw new ProcessorNotFoundError(`Processor ${processorId} not found for twin ${twinId} version ${twinVersion}`);
|
|
458
|
+
}
|
|
459
|
+
const errorText = await response.text();
|
|
460
|
+
throw new Error(`Failed to get processor events: ${response.status} ${response.statusText} - ${errorText}`);
|
|
461
|
+
}
|
|
462
|
+
async listAssets(twinId, offset = 0, limit = 100, options, signal) {
|
|
463
|
+
const includeDeleted = options?.includeDeleted ?? false;
|
|
464
|
+
const params = `offset=${offset}&limit=${limit}${includeDeleted ? '&includeDeleted=true' : ''}`;
|
|
465
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets?${params}`, {
|
|
466
|
+
headers: { Accept: 'application/json' },
|
|
467
|
+
signal
|
|
468
|
+
});
|
|
469
|
+
if (response.ok) {
|
|
470
|
+
return (await response.json());
|
|
471
|
+
}
|
|
472
|
+
if (response.status === 400) {
|
|
473
|
+
const errorBody = await readErrorBody(response);
|
|
474
|
+
throw new AssetValidationError(`Asset listing validation failed: ${errorBody.detail}`, errorBody);
|
|
475
|
+
}
|
|
476
|
+
if (response.status === 404) {
|
|
477
|
+
throw new AssetNotFoundError(`Twin ${twinId} not found`);
|
|
478
|
+
}
|
|
479
|
+
const errorText = await response.text();
|
|
480
|
+
throw new Error(`Failed to list assets: ${response.status} ${response.statusText} - ${errorText}`);
|
|
481
|
+
}
|
|
482
|
+
async getAsset(twinId, assetId, signal) {
|
|
483
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
|
|
484
|
+
headers: { Accept: 'application/json' },
|
|
485
|
+
signal
|
|
486
|
+
});
|
|
487
|
+
if (response.ok) {
|
|
488
|
+
return (await response.json());
|
|
489
|
+
}
|
|
490
|
+
if (response.status === 404) {
|
|
491
|
+
throw new AssetNotFoundError(`Asset ${assetId} not found`);
|
|
492
|
+
}
|
|
493
|
+
const errorText = await response.text();
|
|
494
|
+
throw new Error(`Failed to get asset: ${response.status} ${response.statusText} - ${errorText}`);
|
|
495
|
+
}
|
|
496
|
+
async createAssets(twinId, assets, signal) {
|
|
497
|
+
const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets`, {
|
|
498
|
+
headers: { 'Content-Type': 'application/json' },
|
|
499
|
+
body: JSON.stringify(assets),
|
|
500
|
+
signal
|
|
501
|
+
});
|
|
502
|
+
if (response.status === 201) {
|
|
503
|
+
return (await response.json());
|
|
504
|
+
}
|
|
505
|
+
if (response.status === 400) {
|
|
506
|
+
const errorBody = await readErrorBody(response);
|
|
507
|
+
throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
|
|
508
|
+
}
|
|
509
|
+
const errorText = await response.text();
|
|
510
|
+
throw new Error(`Failed to create assets: ${response.status} ${response.statusText} - ${errorText}`);
|
|
511
|
+
}
|
|
512
|
+
async updateAsset(twinId, assetId, asset, signal) {
|
|
513
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
|
|
514
|
+
headers: { 'Content-Type': 'application/json' },
|
|
515
|
+
body: JSON.stringify(asset),
|
|
516
|
+
signal
|
|
517
|
+
});
|
|
518
|
+
if (response.ok) {
|
|
519
|
+
return (await response.json());
|
|
520
|
+
}
|
|
521
|
+
if (response.status === 409) {
|
|
522
|
+
const conflictedAsset = (await response.json());
|
|
523
|
+
throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
|
|
524
|
+
}
|
|
525
|
+
if (response.status === 400) {
|
|
526
|
+
const errorBody = await readErrorBody(response);
|
|
527
|
+
throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
|
|
528
|
+
}
|
|
529
|
+
const errorText = await response.text();
|
|
530
|
+
throw new Error(`Failed to update asset: ${response.status} ${response.statusText} - ${errorText}`);
|
|
531
|
+
}
|
|
532
|
+
async deleteAsset(twinId, assetId, etag, signal) {
|
|
533
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
|
|
534
|
+
headers: { 'Content-Type': 'application/json' },
|
|
535
|
+
body: JSON.stringify({ etag }),
|
|
536
|
+
signal
|
|
537
|
+
});
|
|
538
|
+
if (response.ok) {
|
|
539
|
+
return (await response.json());
|
|
540
|
+
}
|
|
541
|
+
if (response.status === 409) {
|
|
542
|
+
const conflictedAsset = (await response.json());
|
|
543
|
+
throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
|
|
544
|
+
}
|
|
545
|
+
if (response.status === 404) {
|
|
546
|
+
throw new AssetNotFoundError(`Asset ${assetId} not found`);
|
|
547
|
+
}
|
|
548
|
+
const errorText = await response.text();
|
|
549
|
+
throw new Error(`Failed to delete asset: ${response.status} ${response.statusText} - ${errorText}`);
|
|
550
|
+
}
|
|
551
|
+
async restoreAsset(twinId, assetId, etag) {
|
|
552
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/restore`, {
|
|
553
|
+
headers: { 'Content-Type': 'application/json' },
|
|
554
|
+
body: JSON.stringify({ etag })
|
|
555
|
+
});
|
|
556
|
+
if (response.ok) {
|
|
557
|
+
return (await response.json());
|
|
558
|
+
}
|
|
559
|
+
if (response.status === 409) {
|
|
560
|
+
const conflictedAsset = (await response.json());
|
|
561
|
+
throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
|
|
562
|
+
}
|
|
563
|
+
if (response.status === 404) {
|
|
564
|
+
throw new AssetNotFoundError(`Asset ${assetId} not found`);
|
|
565
|
+
}
|
|
566
|
+
const errorText = await response.text();
|
|
567
|
+
throw new Error(`Failed to restore asset: ${response.status} ${response.statusText} - ${errorText}`);
|
|
568
|
+
}
|
|
569
|
+
async batchAssets(twinId, operations, signal) {
|
|
570
|
+
const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch`, {
|
|
571
|
+
headers: { 'Content-Type': 'application/json' },
|
|
572
|
+
body: JSON.stringify({ operations }),
|
|
573
|
+
signal
|
|
574
|
+
});
|
|
575
|
+
if (response.ok) {
|
|
576
|
+
return (await response.json());
|
|
577
|
+
}
|
|
578
|
+
// The body decides, not the status: a rolled-back batch is reported as a batch error at
|
|
579
|
+
// whatever status its last failing item carried, and only a body that is not a batch
|
|
580
|
+
// response falls through to the status arms below.
|
|
581
|
+
const batchResponse = await readBatchResponse(response);
|
|
582
|
+
if (batchResponse) {
|
|
583
|
+
throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, batchResponse);
|
|
584
|
+
}
|
|
585
|
+
if (response.status === 400) {
|
|
586
|
+
const errorBody = await readErrorBody(response);
|
|
587
|
+
throw new AssetValidationError(`Asset batch validation failed: ${errorBody.detail}`, errorBody);
|
|
588
|
+
}
|
|
589
|
+
if (response.status === 403) {
|
|
590
|
+
const errorText = await response.text();
|
|
591
|
+
throw new AssetForbiddenError(`Asset batch forbidden: ${errorText}`);
|
|
592
|
+
}
|
|
593
|
+
// Reached only where the probe above could not read the body -- a Response without clone(),
|
|
594
|
+
// which some test doubles are. These two statuses only ever carry a batch response, so read
|
|
595
|
+
// it directly rather than losing the per-item results to the generic error below.
|
|
596
|
+
if (response.status === 404 || response.status === 409) {
|
|
597
|
+
const rolledBack = (await response.json());
|
|
598
|
+
throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, rolledBack);
|
|
599
|
+
}
|
|
600
|
+
const errorText = await response.text();
|
|
601
|
+
throw new Error(`Failed to execute asset batch: ${response.status} ${response.statusText} - ${errorText}`);
|
|
602
|
+
}
|
|
603
|
+
async batchGetAssets(twinId, ids, signal) {
|
|
604
|
+
const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch-get`, {
|
|
605
|
+
headers: { 'Content-Type': 'application/json' },
|
|
606
|
+
body: JSON.stringify({ ids }),
|
|
607
|
+
signal
|
|
608
|
+
});
|
|
609
|
+
if (response.ok) {
|
|
610
|
+
return (await response.json());
|
|
611
|
+
}
|
|
612
|
+
if (response.status === 400) {
|
|
613
|
+
const errorBody = await readErrorBody(response);
|
|
614
|
+
throw new AssetValidationError(`Asset batch-get validation failed: ${errorBody.detail}`, errorBody);
|
|
615
|
+
}
|
|
616
|
+
const errorText = await response.text();
|
|
617
|
+
throw new Error(`Failed to batch-get assets: ${response.status} ${response.statusText} - ${errorText}`);
|
|
618
|
+
}
|
|
619
|
+
// ────────── Asset attachments ──────────
|
|
620
|
+
async listAssetAttachments(twinId, assetId, options, signal) {
|
|
621
|
+
const params = new URLSearchParams();
|
|
622
|
+
if (options?.includeDeleted)
|
|
623
|
+
params.set('includeDeleted', 'true');
|
|
624
|
+
if (options?.offset !== undefined)
|
|
625
|
+
params.set('offset', String(options.offset));
|
|
626
|
+
if (options?.limit !== undefined)
|
|
627
|
+
params.set('limit', String(options.limit));
|
|
628
|
+
if (options?.page)
|
|
629
|
+
params.set('page', options.page);
|
|
630
|
+
const url = `${this._baseUrl}/${twinId}/assets/${assetId}/attachments${params.toString() ? `?${params}` : ''}`;
|
|
631
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
632
|
+
headers: { Accept: 'application/json' },
|
|
633
|
+
signal
|
|
634
|
+
});
|
|
635
|
+
if (response.ok)
|
|
636
|
+
return (await response.json());
|
|
637
|
+
if (response.status === 404)
|
|
638
|
+
throw new AssetNotFoundError(`Asset ${assetId} not found`);
|
|
639
|
+
if (response.status === 400) {
|
|
640
|
+
const errorBody = await readErrorBody(response);
|
|
641
|
+
throw new AssetValidationError(`Attachment listing failed: ${errorBody.detail}`, errorBody);
|
|
642
|
+
}
|
|
643
|
+
const errorText = await response.text();
|
|
644
|
+
throw new Error(`Failed to list attachments: ${response.status} ${response.statusText} - ${errorText}`);
|
|
645
|
+
}
|
|
646
|
+
async getAssetAttachmentBlob(twinId, assetId, attachmentId, signal) {
|
|
647
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, { signal });
|
|
648
|
+
if (response.ok)
|
|
649
|
+
return response;
|
|
650
|
+
if (response.status === 404)
|
|
651
|
+
throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
|
|
652
|
+
const errorText = await response.text();
|
|
653
|
+
throw new Error(`Failed to fetch attachment blob: ${response.status} ${response.statusText} - ${errorText}`);
|
|
654
|
+
}
|
|
655
|
+
async createAssetAttachmentUploadSession(twinId, assetId, attachmentId, body, signal) {
|
|
656
|
+
const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/createAttachmentUploadSession`, {
|
|
657
|
+
headers: { 'Content-Type': 'application/json' },
|
|
658
|
+
body: JSON.stringify(body),
|
|
659
|
+
signal
|
|
660
|
+
});
|
|
661
|
+
if (response.ok)
|
|
662
|
+
return (await response.json());
|
|
663
|
+
if (response.status === 409) {
|
|
664
|
+
const conflicted = (await response.json());
|
|
665
|
+
throw new AssetAttachmentConflictError(`Attachment ${attachmentId} already exists; pick a new id for retries`, conflicted);
|
|
666
|
+
}
|
|
667
|
+
if (response.status === 404)
|
|
668
|
+
throw new AssetNotFoundError(`Asset ${assetId} not found`);
|
|
669
|
+
if (response.status === 400) {
|
|
670
|
+
const errorBody = await readErrorBody(response);
|
|
671
|
+
throw new AssetValidationError(`Upload session creation failed: ${errorBody.detail}`, errorBody);
|
|
672
|
+
}
|
|
673
|
+
const errorText = await response.text();
|
|
674
|
+
throw new Error(`Failed to create upload session: ${response.status} ${response.statusText} - ${errorText}`);
|
|
675
|
+
}
|
|
676
|
+
async uploadAssetAttachmentFile(session, file, signal) {
|
|
677
|
+
// The token is encoded as a query param on the upload URL; we lift it
|
|
678
|
+
// out for the caller so they can poll uploadStatus without re-parsing.
|
|
679
|
+
const tokenMatch = session.uploadUrl.match(/token=([^&]+)/);
|
|
680
|
+
if (!tokenMatch) {
|
|
681
|
+
throw new Error(`Upload session URL is missing a token query parameter: ${session.uploadUrl}`);
|
|
682
|
+
}
|
|
683
|
+
const token = tokenMatch[1];
|
|
684
|
+
const chunkSize = Math.max(1, session.maxChunkSize || file.size);
|
|
685
|
+
const total = file.size;
|
|
686
|
+
let offset = 0;
|
|
687
|
+
// Even an empty file needs at least one PUT so the upload session
|
|
688
|
+
// gets a finalize signal. uploadChunks() in the integration tests
|
|
689
|
+
// unconditionally PUTs at least one chunk too.
|
|
690
|
+
do {
|
|
691
|
+
const end = Math.min(offset + chunkSize, total);
|
|
692
|
+
const chunk = file.slice(offset, end);
|
|
693
|
+
const lastByte = Math.max(0, end - 1);
|
|
694
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, session.uploadUrl, {
|
|
695
|
+
headers: { 'Content-Range': `bytes ${offset}-${lastByte}/${total}` },
|
|
696
|
+
body: chunk,
|
|
697
|
+
signal
|
|
698
|
+
});
|
|
699
|
+
if (!response.ok) {
|
|
700
|
+
const errorText = await response.text();
|
|
701
|
+
throw new Error(`Upload chunk failed at offset ${offset}: ${response.status} ${response.statusText} - ${errorText}`);
|
|
702
|
+
}
|
|
703
|
+
offset = end;
|
|
704
|
+
} while (offset < total);
|
|
705
|
+
return { token };
|
|
706
|
+
}
|
|
707
|
+
async getAssetAttachmentUploadStatus(twinId, assetId, token, signal) {
|
|
708
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/uploadStatus/${encodeURIComponent(token)}`, { headers: { Accept: 'application/json' }, signal });
|
|
709
|
+
if (response.ok)
|
|
710
|
+
return (await response.json());
|
|
711
|
+
if (response.status === 404)
|
|
712
|
+
return { status: 'NotFound' };
|
|
713
|
+
const errorText = await response.text();
|
|
714
|
+
throw new Error(`Failed to get upload status: ${response.status} ${response.statusText} - ${errorText}`);
|
|
715
|
+
}
|
|
716
|
+
async putAssetAttachment(twinId, assetId, attachmentId, body, signal) {
|
|
717
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, {
|
|
718
|
+
headers: { 'Content-Type': 'application/json' },
|
|
719
|
+
body: JSON.stringify(body),
|
|
720
|
+
signal
|
|
721
|
+
});
|
|
722
|
+
if (response.ok)
|
|
723
|
+
return (await response.json());
|
|
724
|
+
if (response.status === 409) {
|
|
725
|
+
const conflicted = (await response.json());
|
|
726
|
+
throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
|
|
727
|
+
}
|
|
728
|
+
if (response.status === 404)
|
|
729
|
+
throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
|
|
730
|
+
if (response.status === 400) {
|
|
731
|
+
const errorBody = await readErrorBody(response);
|
|
732
|
+
throw new AssetValidationError(`Attachment update failed: ${errorBody.detail}`, errorBody);
|
|
733
|
+
}
|
|
734
|
+
const errorText = await response.text();
|
|
735
|
+
throw new Error(`Failed to update attachment: ${response.status} ${response.statusText} - ${errorText}`);
|
|
736
|
+
}
|
|
737
|
+
async deleteAssetAttachment(twinId, assetId, attachmentId, etag, signal) {
|
|
738
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, {
|
|
739
|
+
headers: { 'If-Match': etag },
|
|
740
|
+
signal
|
|
741
|
+
});
|
|
742
|
+
if (response.ok)
|
|
743
|
+
return (await response.json());
|
|
744
|
+
if (response.status === 409) {
|
|
745
|
+
const conflicted = (await response.json());
|
|
746
|
+
throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
|
|
747
|
+
}
|
|
748
|
+
if (response.status === 404)
|
|
749
|
+
throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
|
|
750
|
+
const errorText = await response.text();
|
|
751
|
+
throw new Error(`Failed to delete attachment: ${response.status} ${response.statusText} - ${errorText}`);
|
|
752
|
+
}
|
|
753
|
+
async restoreAssetAttachment(twinId, assetId, attachmentId, etag, signal) {
|
|
754
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/restore`, {
|
|
755
|
+
headers: { 'Content-Type': 'application/json' },
|
|
756
|
+
body: JSON.stringify({ etag }),
|
|
757
|
+
signal
|
|
758
|
+
});
|
|
759
|
+
if (response.ok)
|
|
760
|
+
return (await response.json());
|
|
761
|
+
if (response.status === 409) {
|
|
762
|
+
const conflicted = (await response.json());
|
|
763
|
+
throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
|
|
764
|
+
}
|
|
765
|
+
if (response.status === 404)
|
|
766
|
+
throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
|
|
767
|
+
const errorText = await response.text();
|
|
768
|
+
throw new Error(`Failed to restore attachment: ${response.status} ${response.statusText} - ${errorText}`);
|
|
769
|
+
}
|
|
770
|
+
async listTwinFiles(twinId, options = {}, signal) {
|
|
771
|
+
const { includeDeleted = false, checkedIn = false, version, page = undefined, limit = 100 } = options;
|
|
772
|
+
if (version !== undefined && !checkedIn) {
|
|
773
|
+
throw new Error('The "version" option can only be used when "checkedIn" is true.');
|
|
774
|
+
}
|
|
775
|
+
const params = new URLSearchParams();
|
|
776
|
+
if (includeDeleted)
|
|
777
|
+
params.set('includeDeleted', 'true');
|
|
778
|
+
if (checkedIn)
|
|
779
|
+
params.set('checkedIn', 'true');
|
|
780
|
+
if (version !== undefined)
|
|
781
|
+
params.set('version', version.toString());
|
|
782
|
+
if (page !== undefined)
|
|
783
|
+
params.set('page', page);
|
|
784
|
+
if (limit !== 100)
|
|
785
|
+
params.set('limit', limit.toString());
|
|
786
|
+
const url = params.toString()
|
|
787
|
+
? `${this._baseUrl}/${twinId}/files?${params}`
|
|
788
|
+
: `${this._baseUrl}/${twinId}/files`;
|
|
789
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
790
|
+
headers: { Accept: 'application/json' },
|
|
791
|
+
signal
|
|
792
|
+
});
|
|
793
|
+
if (response.ok) {
|
|
794
|
+
return (await response.json());
|
|
795
|
+
}
|
|
796
|
+
if (response.status === 400) {
|
|
797
|
+
const errorBody = await readErrorBody(response);
|
|
798
|
+
throw new TwinFileValidationError(`Twin file listing validation failed: ${errorBody.detail}`, errorBody);
|
|
799
|
+
}
|
|
800
|
+
const errorText = await response.text();
|
|
801
|
+
throw new Error(`Failed to list twin files: ${response.status} ${response.statusText} - ${errorText}`);
|
|
802
|
+
}
|
|
803
|
+
async getTwinFile(twinId, fileId, signal) {
|
|
804
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}`, {
|
|
805
|
+
headers: { Accept: 'application/json' },
|
|
806
|
+
signal
|
|
807
|
+
});
|
|
808
|
+
if (response.ok) {
|
|
809
|
+
return (await response.json());
|
|
810
|
+
}
|
|
811
|
+
if (response.status === 404) {
|
|
812
|
+
throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
|
|
813
|
+
}
|
|
814
|
+
if (response.status === 400) {
|
|
815
|
+
const errorText = await response.text();
|
|
816
|
+
throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
|
|
817
|
+
}
|
|
818
|
+
const errorText = await response.text();
|
|
819
|
+
throw new Error(`Failed to get twin file: ${response.status} ${response.statusText} - ${errorText}`);
|
|
820
|
+
}
|
|
821
|
+
async createOrUpdateTwinFile(twinId, fileId, twinFile, signal) {
|
|
822
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
|
|
823
|
+
headers: { 'Content-Type': 'application/json' },
|
|
824
|
+
body: JSON.stringify(twinFile),
|
|
825
|
+
signal
|
|
826
|
+
});
|
|
827
|
+
if (response.ok || response.status === 201) {
|
|
828
|
+
return (await response.json());
|
|
829
|
+
}
|
|
830
|
+
if (response.status === 400) {
|
|
831
|
+
const errorBody = await readErrorBody(response);
|
|
832
|
+
throw new TwinFileValidationError(`Twin file validation failed`, errorBody);
|
|
833
|
+
}
|
|
834
|
+
if (response.status === 409) {
|
|
835
|
+
const conflictedFile = (await response.json());
|
|
836
|
+
throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
|
|
837
|
+
}
|
|
838
|
+
const errorText = await response.text();
|
|
839
|
+
throw new Error(`Failed to create/update twin file: ${response.status} ${response.statusText} - ${errorText}`);
|
|
840
|
+
}
|
|
841
|
+
async deleteTwinFile(twinId, fileId, deleteRequest, signal) {
|
|
842
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/files/${fileId}`, {
|
|
843
|
+
headers: { 'Content-Type': 'application/json' },
|
|
844
|
+
body: JSON.stringify(deleteRequest),
|
|
845
|
+
signal
|
|
846
|
+
});
|
|
847
|
+
if (response.ok) {
|
|
848
|
+
return (await response.json());
|
|
849
|
+
}
|
|
850
|
+
if (response.status === 404) {
|
|
851
|
+
throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
|
|
852
|
+
}
|
|
853
|
+
if (response.status === 409) {
|
|
854
|
+
const conflictedFile = (await response.json());
|
|
855
|
+
throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
|
|
856
|
+
}
|
|
857
|
+
if (response.status === 400) {
|
|
858
|
+
const errorText = await response.text();
|
|
859
|
+
throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
|
|
860
|
+
}
|
|
861
|
+
const errorText = await response.text();
|
|
862
|
+
throw new Error(`Failed to delete twin file: ${response.status} ${response.statusText} - ${errorText}`);
|
|
863
|
+
}
|
|
864
|
+
async restoreTwinFile(twinId, fileId, etag, signal) {
|
|
865
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}/restore`, {
|
|
866
|
+
headers: { 'Content-Type': 'application/json' },
|
|
867
|
+
body: JSON.stringify({ etag }),
|
|
868
|
+
signal
|
|
869
|
+
});
|
|
870
|
+
if (response.ok) {
|
|
871
|
+
return (await response.json());
|
|
872
|
+
}
|
|
873
|
+
if (response.status === 404) {
|
|
874
|
+
throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
|
|
875
|
+
}
|
|
876
|
+
if (response.status === 409) {
|
|
877
|
+
const conflictedFile = (await response.json());
|
|
878
|
+
throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
|
|
879
|
+
}
|
|
880
|
+
if (response.status === 400) {
|
|
881
|
+
const errorText = await response.text();
|
|
882
|
+
throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
|
|
883
|
+
}
|
|
884
|
+
const errorText = await response.text();
|
|
885
|
+
throw new Error(`Failed to restore twin file: ${response.status} ${response.statusText} - ${errorText}`);
|
|
886
|
+
}
|
|
887
|
+
async getTwinFileContent(twinId, fileId, signal) {
|
|
888
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/content`, { signal });
|
|
889
|
+
if (response.ok) {
|
|
890
|
+
return await response.blob();
|
|
891
|
+
}
|
|
892
|
+
if (response.status === 404) {
|
|
893
|
+
throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
|
|
894
|
+
}
|
|
895
|
+
const errorText = await response.text();
|
|
896
|
+
throw new Error(`Failed to get twin file content: ${response.status} ${response.statusText} - ${errorText}`);
|
|
897
|
+
}
|
|
898
|
+
async getTwinFileVersion(twinId, fileId, version, signal) {
|
|
899
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}`, { signal });
|
|
900
|
+
if (response.ok) {
|
|
901
|
+
return (await response.json());
|
|
902
|
+
}
|
|
903
|
+
if (response.status === 404) {
|
|
904
|
+
throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} not found`);
|
|
905
|
+
}
|
|
906
|
+
const errorText = await response.text();
|
|
907
|
+
throw new Error(`Failed to get twin file version: ${response.status} ${response.statusText} - ${errorText}`);
|
|
908
|
+
}
|
|
909
|
+
async listTwinFileVersions(twinId, fileId, options, signal) {
|
|
910
|
+
const params = new URLSearchParams();
|
|
911
|
+
if (options?.includeDeleted !== undefined)
|
|
912
|
+
params.append('includeDeleted', String(options.includeDeleted));
|
|
913
|
+
if (options?.offset !== undefined)
|
|
914
|
+
params.append('offset', String(options.offset));
|
|
915
|
+
if (options?.limit !== undefined)
|
|
916
|
+
params.append('limit', String(options.limit));
|
|
917
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/versions?${params.toString()}`, { signal });
|
|
918
|
+
if (response.ok) {
|
|
919
|
+
return (await response.json());
|
|
920
|
+
}
|
|
921
|
+
if (response.status === 400) {
|
|
922
|
+
const errorBody = await readErrorBody(response);
|
|
923
|
+
throw new TwinFileValidationError(`Twin file version listing validation failed: ${errorBody.detail}`, errorBody);
|
|
924
|
+
}
|
|
925
|
+
const errorText = await response.text();
|
|
926
|
+
throw new Error(`Failed to list twin file versions: ${response.status} ${response.statusText} - ${errorText}`);
|
|
927
|
+
}
|
|
928
|
+
async getTwinFileVersionContent(twinId, fileId, version, signal) {
|
|
929
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}/content`, { signal });
|
|
930
|
+
if (response.ok) {
|
|
931
|
+
return await response.blob();
|
|
932
|
+
}
|
|
933
|
+
if (response.status === 404) {
|
|
934
|
+
throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} content not found`);
|
|
935
|
+
}
|
|
936
|
+
const errorText = await response.text();
|
|
937
|
+
throw new Error(`Failed to get twin file version content: ${response.status} ${response.statusText} - ${errorText}`);
|
|
938
|
+
}
|
|
939
|
+
async createOrUpdateTwinRaw(twinId, twin, signal) {
|
|
940
|
+
return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
|
|
941
|
+
headers: { 'Content-Type': 'application/json' },
|
|
942
|
+
body: JSON.stringify(twin),
|
|
943
|
+
signal
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
async createOrUpdateTwinFileRaw(twinId, fileId, twinFile, signal) {
|
|
947
|
+
return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
|
|
948
|
+
headers: { 'Content-Type': 'application/json' },
|
|
949
|
+
body: JSON.stringify(twinFile),
|
|
950
|
+
signal
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
// ---------- Asset categories ----------
|
|
954
|
+
async getMyPermissions(signal) {
|
|
955
|
+
const url = `${this._baseUrl}/permissions/me`;
|
|
956
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
957
|
+
headers: { Accept: 'application/json' },
|
|
958
|
+
signal
|
|
959
|
+
});
|
|
960
|
+
if (response.ok) {
|
|
961
|
+
return (await response.json());
|
|
962
|
+
}
|
|
963
|
+
// 401: no valid auth context. Throw so consumers can redirect to
|
|
964
|
+
// login / refresh tokens rather than mistake an expired session for
|
|
965
|
+
// "logged-in user with no grants."
|
|
966
|
+
if (response.status === 401) {
|
|
967
|
+
throw new TwinUnauthenticatedError('Not authenticated');
|
|
968
|
+
}
|
|
969
|
+
// 403: authenticated but no Twin-service capabilities. Surface as an
|
|
970
|
+
// empty array so consumers can treat it like a logged-in user with
|
|
971
|
+
// zero grants.
|
|
972
|
+
if (response.status === 403) {
|
|
973
|
+
return { permissions: [] };
|
|
974
|
+
}
|
|
975
|
+
throw new Error(`Failed to fetch permissions: ${response.status} ${response.statusText}`);
|
|
976
|
+
}
|
|
977
|
+
// ---------- Twin-scoped permissions (TWINS-2982) ----------
|
|
978
|
+
async getMyTwinPermissions(twinId, signal) {
|
|
979
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/permissions/me`, {
|
|
980
|
+
headers: { Accept: 'application/json' },
|
|
981
|
+
signal
|
|
982
|
+
});
|
|
983
|
+
if (response.ok)
|
|
984
|
+
return (await response.json());
|
|
985
|
+
return this._failTwinPermissionsRequest(response, 'read the capabilities on the twin', twinId);
|
|
986
|
+
}
|
|
987
|
+
async listTwinPermissions(twinId, options, signal) {
|
|
988
|
+
const params = new URLSearchParams();
|
|
989
|
+
if (options?.page !== undefined)
|
|
990
|
+
params.set('page', options.page);
|
|
991
|
+
if (options?.limit !== undefined)
|
|
992
|
+
params.set('limit', options.limit.toString());
|
|
993
|
+
const query = params.toString();
|
|
994
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/permissions${query ? `?${query}` : ''}`, { headers: { Accept: 'application/json' }, signal });
|
|
995
|
+
if (response.ok)
|
|
996
|
+
return (await response.json());
|
|
997
|
+
return this._failTwinPermissionsRequest(response, 'list the twin permissions', twinId);
|
|
998
|
+
}
|
|
999
|
+
async listAllTwinPermissions(twinId, signal) {
|
|
1000
|
+
const limit = 1000;
|
|
1001
|
+
const entries = [];
|
|
1002
|
+
// Paged on a short page rather than on `nextPage`, which the service emits even for the
|
|
1003
|
+
// last page — the same reason `listAllTwins` pages this way.
|
|
1004
|
+
let page;
|
|
1005
|
+
let received;
|
|
1006
|
+
do {
|
|
1007
|
+
const response = await this.listTwinPermissions(twinId, { page, limit }, signal);
|
|
1008
|
+
entries.push(...response.data);
|
|
1009
|
+
received = response.data.length;
|
|
1010
|
+
page = response.pagination.nextPage;
|
|
1011
|
+
} while (received >= limit);
|
|
1012
|
+
return entries;
|
|
1013
|
+
}
|
|
1014
|
+
async setTwinPermissions(twinId, principalId, body, signal) {
|
|
1015
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/permissions/${principalId}`, {
|
|
1016
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
1017
|
+
body: JSON.stringify(body),
|
|
1018
|
+
signal
|
|
1019
|
+
});
|
|
1020
|
+
if (response.ok)
|
|
1021
|
+
return (await response.json());
|
|
1022
|
+
return this._failTwinPermissionsRequest(response, 'set the twin permissions', twinId, {
|
|
1023
|
+
id: principalId,
|
|
1024
|
+
label: 'Principal'
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
async deleteTwinPermissions(twinId, principalId, etag, signal) {
|
|
1028
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/permissions/${principalId}`, {
|
|
1029
|
+
// The etag travels in the header, which is the transport the surface prefers and
|
|
1030
|
+
// the one that wins when both are present. It is repeated in the body — the other
|
|
1031
|
+
// carrier ADR-0008 documents, and the one the endpoint binds — so that the request
|
|
1032
|
+
// still names an etag if a proxy strips the header, or drops the body of a DELETE.
|
|
1033
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'If-Match': etag },
|
|
1034
|
+
body: JSON.stringify({ etag }),
|
|
1035
|
+
signal
|
|
1036
|
+
});
|
|
1037
|
+
if (response.ok)
|
|
1038
|
+
return (await response.json());
|
|
1039
|
+
return this._failTwinPermissionsRequest(response, 'revoke the twin permissions', twinId, {
|
|
1040
|
+
id: principalId,
|
|
1041
|
+
label: 'Principal'
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
async listTwinGroups(twinId, signal) {
|
|
1045
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/groups`, {
|
|
1046
|
+
headers: { Accept: 'application/json' },
|
|
1047
|
+
signal
|
|
1048
|
+
});
|
|
1049
|
+
if (response.ok)
|
|
1050
|
+
return (await response.json());
|
|
1051
|
+
return this._failTwinPermissionsRequest(response, "list the twin's groups", twinId);
|
|
1052
|
+
}
|
|
1053
|
+
async getTwinGroup(twinId, groupId, signal) {
|
|
1054
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/groups/${groupId}`, {
|
|
1055
|
+
headers: { Accept: 'application/json' },
|
|
1056
|
+
signal
|
|
1057
|
+
});
|
|
1058
|
+
if (response.ok)
|
|
1059
|
+
return (await response.json());
|
|
1060
|
+
return this._failTwinPermissionsRequest(response, "get the twin's group", twinId, {
|
|
1061
|
+
id: groupId,
|
|
1062
|
+
label: 'Group'
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
// ---------- Asset-domain permissions ----------
|
|
1066
|
+
async listAssetPermissions(options, signal) {
|
|
1067
|
+
const params = new URLSearchParams();
|
|
1068
|
+
if (options?.page !== undefined)
|
|
1069
|
+
params.set('page', options.page);
|
|
1070
|
+
if (options?.limit !== undefined)
|
|
1071
|
+
params.set('limit', options.limit.toString());
|
|
1072
|
+
const query = params.toString();
|
|
1073
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/permissions${query ? `?${query}` : ''}`, { headers: { Accept: 'application/json' }, signal });
|
|
1074
|
+
if (response.ok)
|
|
1075
|
+
return (await response.json());
|
|
1076
|
+
return this._failAssetPermissionsRequest(response, 'list the asset permissions');
|
|
1077
|
+
}
|
|
1078
|
+
async listAllAssetPermissions(signal) {
|
|
1079
|
+
const limit = 1000;
|
|
1080
|
+
const entries = [];
|
|
1081
|
+
// Paged on a short page rather than on `nextPage`, which the service emits even for the
|
|
1082
|
+
// last page — the same reason `listAllTwinPermissions` pages this way.
|
|
1083
|
+
let page;
|
|
1084
|
+
let received;
|
|
1085
|
+
do {
|
|
1086
|
+
const response = await this.listAssetPermissions({ page, limit }, signal);
|
|
1087
|
+
entries.push(...response.data);
|
|
1088
|
+
received = response.data.length;
|
|
1089
|
+
page = response.pagination.nextPage;
|
|
1090
|
+
} while (received >= limit);
|
|
1091
|
+
return entries;
|
|
1092
|
+
}
|
|
1093
|
+
async setAssetPermissions(principalId, body, signal) {
|
|
1094
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/permissions/${principalId}`, {
|
|
1095
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
1096
|
+
body: JSON.stringify(body),
|
|
1097
|
+
signal
|
|
1098
|
+
});
|
|
1099
|
+
if (response.ok)
|
|
1100
|
+
return (await response.json());
|
|
1101
|
+
return this._failAssetPermissionsRequest(response, 'set the asset permissions', {
|
|
1102
|
+
id: principalId,
|
|
1103
|
+
label: 'Principal'
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
async deleteAssetPermissions(principalId, etag, signal) {
|
|
1107
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/permissions/${principalId}`, {
|
|
1108
|
+
// The etag travels in the header, which is the transport the surface prefers and the
|
|
1109
|
+
// one that wins when both are present. It is repeated in the body — the other
|
|
1110
|
+
// carrier the endpoint binds — so the request still names an etag if a proxy strips
|
|
1111
|
+
// the header, or drops the body of a DELETE.
|
|
1112
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'If-Match': etag },
|
|
1113
|
+
body: JSON.stringify({ etag }),
|
|
1114
|
+
signal
|
|
1115
|
+
});
|
|
1116
|
+
if (response.ok)
|
|
1117
|
+
return (await response.json());
|
|
1118
|
+
return this._failAssetPermissionsRequest(response, 'revoke the asset permissions', {
|
|
1119
|
+
id: principalId,
|
|
1120
|
+
label: 'Principal'
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
async listAssetGroups(signal) {
|
|
1124
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/groups`, {
|
|
1125
|
+
headers: { Accept: 'application/json' },
|
|
1126
|
+
signal
|
|
1127
|
+
});
|
|
1128
|
+
if (response.ok)
|
|
1129
|
+
return (await response.json());
|
|
1130
|
+
return this._failAssetPermissionsRequest(response, "list the asset domain's default groups");
|
|
1131
|
+
}
|
|
1132
|
+
async getAssetGroup(name, signal) {
|
|
1133
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/groups/${encodeURIComponent(name)}`, { headers: { Accept: 'application/json' }, signal });
|
|
1134
|
+
if (response.ok)
|
|
1135
|
+
return (await response.json());
|
|
1136
|
+
return this._failAssetPermissionsRequest(response, "get the asset domain's default group", {
|
|
1137
|
+
id: name,
|
|
1138
|
+
label: 'Group'
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Maps a refusal from the customer-wide asset permission surface onto the typed error its
|
|
1143
|
+
* status and cause name. The twin-scoped counterpart of this cannot be reused: its 404s are
|
|
1144
|
+
* about a twin, and this surface has none — a 404 naming no principal means the *name* was
|
|
1145
|
+
* unknown, not that a resource was missing.
|
|
1146
|
+
*
|
|
1147
|
+
* A 404 is dispatched on `item.kind`, which is what separates "that group has not been seeded
|
|
1148
|
+
* for this customer yet", which is worth retrying, from "no such group name", which is not.
|
|
1149
|
+
*/
|
|
1150
|
+
async _failAssetPermissionsRequest(response, action, principal) {
|
|
1151
|
+
if (response.status === 401) {
|
|
1152
|
+
throw new TwinUnauthenticatedError('Not authenticated');
|
|
1153
|
+
}
|
|
1154
|
+
const errorBody = await readErrorBody(response);
|
|
1155
|
+
switch (response.status) {
|
|
1156
|
+
case 400:
|
|
1157
|
+
throw new TwinValidationError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1158
|
+
case 403:
|
|
1159
|
+
// The message is the server's own, never a reason invented here: a Twin service that
|
|
1160
|
+
// could not evaluate the request answers a bodiless 403, and `errorBody.cause` is
|
|
1161
|
+
// what separates that from a genuine `InsufficientPermissions`.
|
|
1162
|
+
throw new TwinPermissionsForbiddenError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1163
|
+
case 404:
|
|
1164
|
+
if (errorBody.item?.kind === 'Principal' && principal !== undefined) {
|
|
1165
|
+
throw new TwinPrincipalNotFoundError(`${principal.label} ${principal.id} not found on the asset domain`, errorBody);
|
|
1166
|
+
}
|
|
1167
|
+
throw new TwinNotFoundError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1168
|
+
case 409:
|
|
1169
|
+
throw new AssetPermissionsConflictError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1170
|
+
case 503:
|
|
1171
|
+
throw new TwinTemporarilyUnavailableError(`Failed to ${action}: ${errorBody.detail}`, errorBody, retryAfterSeconds(response));
|
|
1172
|
+
}
|
|
1173
|
+
// A 500 can also mean the Permission service failed: the Twin service answers 503 when the
|
|
1174
|
+
// call threw, but 500 when it returned an upstream failure. Only the 503 half is classified
|
|
1175
|
+
// as retryable here, because the 500 arrives as a ProblemDetails payload with no cause to
|
|
1176
|
+
// recognise it by.
|
|
1177
|
+
throw new Error(`Failed to ${action}: ${response.status} ${response.statusText} - ${errorBody.detail}`);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Maps a refusal from the twin-scoped permission surface onto the typed error its status
|
|
1181
|
+
* and cause name. Shared by all six methods because the surface answers them uniformly:
|
|
1182
|
+
* the status is decided by the request's shape and the caller's grants, not by the route.
|
|
1183
|
+
*
|
|
1184
|
+
* A 404 is dispatched on `item.kind`, which is the only thing separating "no such twin, or
|
|
1185
|
+
* none you may see" from "the principal you named is not there" — the two arrive on the
|
|
1186
|
+
* same status from the same route.
|
|
1187
|
+
*/
|
|
1188
|
+
async _failTwinPermissionsRequest(response, action, twinId, principal) {
|
|
1189
|
+
if (response.status === 401) {
|
|
1190
|
+
throw new TwinUnauthenticatedError('Not authenticated');
|
|
1191
|
+
}
|
|
1192
|
+
const errorBody = await readErrorBody(response);
|
|
1193
|
+
switch (response.status) {
|
|
1194
|
+
case 400:
|
|
1195
|
+
throw new TwinValidationError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1196
|
+
case 403:
|
|
1197
|
+
// The message is the server's own, never a reason invented here: a 403 does not
|
|
1198
|
+
// always mean "you lack ManageTwinPermissions". `/permissions/me` has no capability
|
|
1199
|
+
// gate at all, and a Twin service that could not evaluate the request answers a
|
|
1200
|
+
// bodiless 403 — `errorBody.cause` is what separates those, so it travels with it.
|
|
1201
|
+
throw new TwinPermissionsForbiddenError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1202
|
+
case 404:
|
|
1203
|
+
if (errorBody.item?.kind === 'Principal' && principal !== undefined) {
|
|
1204
|
+
throw new TwinPrincipalNotFoundError(`${principal.label} ${principal.id} not found on twin ${twinId}`, errorBody);
|
|
1205
|
+
}
|
|
1206
|
+
throw new TwinNotFoundError(`Twin ${twinId} not found`, errorBody);
|
|
1207
|
+
case 409:
|
|
1208
|
+
throw new TwinPermissionsConflictError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
|
|
1209
|
+
case 503:
|
|
1210
|
+
throw new TwinTemporarilyUnavailableError(`Failed to ${action}: ${errorBody.detail}`, errorBody, retryAfterSeconds(response));
|
|
1211
|
+
}
|
|
1212
|
+
// Note that a 500 can also mean the Permission service failed: the Twin service answers 503
|
|
1213
|
+
// when the call threw, but 500 when it returned an upstream failure. Only the 503 half is
|
|
1214
|
+
// classified as retryable here, because the 500 arrives as a ProblemDetails payload with no
|
|
1215
|
+
// cause to recognise it by.
|
|
1216
|
+
throw new Error(`Failed to ${action}: ${response.status} ${response.statusText} - ${errorBody.detail}`);
|
|
1217
|
+
}
|
|
1218
|
+
async getAssetTypeMetaSchema(signal) {
|
|
1219
|
+
const url = `${this._assetTypeCatalogUrl}/meta-schema`;
|
|
1220
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
1221
|
+
headers: { Accept: 'application/schema+json, application/json' },
|
|
1222
|
+
signal
|
|
1223
|
+
});
|
|
1224
|
+
if (!response.ok) {
|
|
1225
|
+
throw new Error(`Failed to fetch meta-schema: ${response.status} ${response.statusText}`);
|
|
1226
|
+
}
|
|
1227
|
+
return (await response.json());
|
|
1228
|
+
}
|
|
1229
|
+
async listAssetCategories(options, signal) {
|
|
1230
|
+
const params = new URLSearchParams();
|
|
1231
|
+
if (options?.page !== undefined)
|
|
1232
|
+
params.set('page', options.page);
|
|
1233
|
+
if (options?.limit !== undefined)
|
|
1234
|
+
params.set('limit', options.limit.toString());
|
|
1235
|
+
if (options?.includeDeleted)
|
|
1236
|
+
params.set('includeDeleted', 'true');
|
|
1237
|
+
const url = params.toString()
|
|
1238
|
+
? `${this._assetTypeCatalogUrl}/categories?${params}`
|
|
1239
|
+
: `${this._assetTypeCatalogUrl}/categories`;
|
|
1240
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
1241
|
+
headers: { Accept: 'application/json' },
|
|
1242
|
+
signal
|
|
1243
|
+
});
|
|
1244
|
+
if (response.ok)
|
|
1245
|
+
return (await response.json());
|
|
1246
|
+
if (response.status === 403)
|
|
1247
|
+
throw new AssetCategoryForbiddenError('Asset category listing forbidden');
|
|
1248
|
+
if (response.status === 400) {
|
|
1249
|
+
const errorBody = await readErrorBody(response);
|
|
1250
|
+
throw new AssetCategoryValidationError(`Asset category listing failed: ${errorBody.detail}`, errorBody);
|
|
1251
|
+
}
|
|
1252
|
+
throw new Error(`Failed to list asset categories: ${response.status} ${response.statusText}`);
|
|
1253
|
+
}
|
|
1254
|
+
async getAssetCategory(categoryId, signal) {
|
|
1255
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._assetTypeCatalogUrl}/categories/${categoryId}`, {
|
|
1256
|
+
headers: { Accept: 'application/json' },
|
|
1257
|
+
signal
|
|
1258
|
+
});
|
|
1259
|
+
if (response.ok)
|
|
1260
|
+
return (await response.json());
|
|
1261
|
+
if (response.status === 403)
|
|
1262
|
+
throw new AssetCategoryForbiddenError(`Asset category ${categoryId} read forbidden`);
|
|
1263
|
+
if (response.status === 404)
|
|
1264
|
+
throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
|
|
1265
|
+
throw new Error(`Failed to get asset category: ${response.status} ${response.statusText}`);
|
|
1266
|
+
}
|
|
1267
|
+
async putAssetCategory(categoryId, body, signal) {
|
|
1268
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._assetTypeCatalogUrl}/categories/${categoryId}`, {
|
|
1269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1270
|
+
body: JSON.stringify(body),
|
|
1271
|
+
signal
|
|
1272
|
+
});
|
|
1273
|
+
if (response.ok)
|
|
1274
|
+
return (await response.json());
|
|
1275
|
+
if (response.status === 403)
|
|
1276
|
+
throw new AssetCategoryForbiddenError('Asset category upsert forbidden');
|
|
1277
|
+
if (response.status === 409) {
|
|
1278
|
+
const current = (await response.json());
|
|
1279
|
+
throw new AssetCategoryConflictError('Asset category was modified by another user', current);
|
|
1280
|
+
}
|
|
1281
|
+
if (response.status === 400) {
|
|
1282
|
+
const errorBody = await readErrorBody(response);
|
|
1283
|
+
throw new AssetCategoryValidationError(`Asset category validation failed: ${errorBody.detail}`, errorBody);
|
|
1284
|
+
}
|
|
1285
|
+
const errorText = await response.text();
|
|
1286
|
+
throw new Error(`Failed to upsert asset category: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1287
|
+
}
|
|
1288
|
+
async deleteAssetCategory(categoryId, etag, signal) {
|
|
1289
|
+
return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ false, signal);
|
|
1290
|
+
}
|
|
1291
|
+
async restoreAssetCategory(categoryId, etag, signal) {
|
|
1292
|
+
return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ true, signal);
|
|
1293
|
+
}
|
|
1294
|
+
async _categoryDeleteOrRestore(categoryId, etag, isRestore, signal) {
|
|
1295
|
+
const url = isRestore
|
|
1296
|
+
? `${this._assetTypeCatalogUrl}/categories/${categoryId}/restore`
|
|
1297
|
+
: `${this._assetTypeCatalogUrl}/categories/${categoryId}`;
|
|
1298
|
+
const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
|
|
1299
|
+
const response = await this._httpClient.fetch(method, url, {
|
|
1300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1301
|
+
body: JSON.stringify({ etag }),
|
|
1302
|
+
signal
|
|
1303
|
+
});
|
|
1304
|
+
if (response.ok)
|
|
1305
|
+
return (await response.json());
|
|
1306
|
+
if (response.status === 403) {
|
|
1307
|
+
const op = isRestore ? 'restore' : 'delete';
|
|
1308
|
+
throw new AssetCategoryForbiddenError(`Asset category ${op} forbidden`);
|
|
1309
|
+
}
|
|
1310
|
+
if (response.status === 409) {
|
|
1311
|
+
const current = (await response.json());
|
|
1312
|
+
throw new AssetCategoryConflictError('Asset category was modified by another user', current);
|
|
1313
|
+
}
|
|
1314
|
+
if (response.status === 404)
|
|
1315
|
+
throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
|
|
1316
|
+
const errorText = await response.text();
|
|
1317
|
+
const op = isRestore ? 'restore' : 'delete';
|
|
1318
|
+
throw new Error(`Failed to ${op} asset category: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1319
|
+
}
|
|
1320
|
+
async listAssetCategoryVersions(categoryId, options, signal) {
|
|
1321
|
+
const params = new URLSearchParams();
|
|
1322
|
+
if (options?.page !== undefined)
|
|
1323
|
+
params.set('page', options.page);
|
|
1324
|
+
if (options?.limit !== undefined)
|
|
1325
|
+
params.set('limit', options.limit.toString());
|
|
1326
|
+
if (options?.includeDeleted)
|
|
1327
|
+
params.set('includeDeleted', 'true');
|
|
1328
|
+
const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/versions`;
|
|
1329
|
+
const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
|
|
1330
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
1331
|
+
headers: { Accept: 'application/json' },
|
|
1332
|
+
signal
|
|
1333
|
+
});
|
|
1334
|
+
if (response.ok)
|
|
1335
|
+
return (await response.json());
|
|
1336
|
+
if (response.status === 403)
|
|
1337
|
+
throw new AssetCategoryForbiddenError('Asset category version listing forbidden');
|
|
1338
|
+
if (response.status === 400) {
|
|
1339
|
+
const errorBody = await readErrorBody(response);
|
|
1340
|
+
throw new AssetCategoryValidationError(`Asset category version listing failed: ${errorBody.detail}`, errorBody);
|
|
1341
|
+
}
|
|
1342
|
+
if (response.status === 404)
|
|
1343
|
+
throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
|
|
1344
|
+
throw new Error(`Failed to list asset category versions: ${response.status} ${response.statusText}`);
|
|
1345
|
+
}
|
|
1346
|
+
// ---------- Asset types ----------
|
|
1347
|
+
async listAssetTypes(categoryId, options, signal) {
|
|
1348
|
+
const params = new URLSearchParams();
|
|
1349
|
+
if (options?.page !== undefined)
|
|
1350
|
+
params.set('page', options.page);
|
|
1351
|
+
if (options?.limit !== undefined)
|
|
1352
|
+
params.set('limit', options.limit.toString());
|
|
1353
|
+
if (options?.includeDeleted)
|
|
1354
|
+
params.set('includeDeleted', 'true');
|
|
1355
|
+
const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types`;
|
|
1356
|
+
const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
|
|
1357
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
1358
|
+
headers: { Accept: 'application/json' },
|
|
1359
|
+
signal
|
|
1360
|
+
});
|
|
1361
|
+
if (response.ok)
|
|
1362
|
+
return (await response.json());
|
|
1363
|
+
if (response.status === 403)
|
|
1364
|
+
throw new AssetTypeForbiddenError('Asset type listing forbidden');
|
|
1365
|
+
if (response.status === 400) {
|
|
1366
|
+
const errorBody = await readErrorBody(response);
|
|
1367
|
+
throw new AssetTypeValidationError(`Asset type listing failed: ${errorBody.detail}`, errorBody);
|
|
1368
|
+
}
|
|
1369
|
+
throw new Error(`Failed to list asset types: ${response.status} ${response.statusText}`);
|
|
1370
|
+
}
|
|
1371
|
+
async getAssetType(categoryId, typeId, signal) {
|
|
1372
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`, { headers: { Accept: 'application/json' }, signal });
|
|
1373
|
+
if (response.ok)
|
|
1374
|
+
return (await response.json());
|
|
1375
|
+
if (response.status === 403)
|
|
1376
|
+
throw new AssetTypeForbiddenError(`Asset type ${typeId} read forbidden`);
|
|
1377
|
+
if (response.status === 404)
|
|
1378
|
+
throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
|
|
1379
|
+
throw new Error(`Failed to get asset type: ${response.status} ${response.statusText}`);
|
|
1380
|
+
}
|
|
1381
|
+
async listAssetTypeVersions(categoryId, typeId, options, signal) {
|
|
1382
|
+
const params = new URLSearchParams();
|
|
1383
|
+
if (options?.page !== undefined)
|
|
1384
|
+
params.set('page', options.page);
|
|
1385
|
+
if (options?.limit !== undefined)
|
|
1386
|
+
params.set('limit', options.limit.toString());
|
|
1387
|
+
if (options?.includeDeleted)
|
|
1388
|
+
params.set('includeDeleted', 'true');
|
|
1389
|
+
const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/versions`;
|
|
1390
|
+
const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
|
|
1391
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
1392
|
+
headers: { Accept: 'application/json' },
|
|
1393
|
+
signal
|
|
1394
|
+
});
|
|
1395
|
+
if (response.ok)
|
|
1396
|
+
return (await response.json());
|
|
1397
|
+
if (response.status === 403)
|
|
1398
|
+
throw new AssetTypeForbiddenError('Asset type version listing forbidden');
|
|
1399
|
+
if (response.status === 400) {
|
|
1400
|
+
const errorBody = await readErrorBody(response);
|
|
1401
|
+
throw new AssetTypeValidationError(`Asset type version listing failed: ${errorBody.detail}`, errorBody);
|
|
1402
|
+
}
|
|
1403
|
+
if (response.status === 404)
|
|
1404
|
+
throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
|
|
1405
|
+
throw new Error(`Failed to list asset type versions: ${response.status} ${response.statusText}`);
|
|
1406
|
+
}
|
|
1407
|
+
async putAssetType(categoryId, typeId, body, signal) {
|
|
1408
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`, {
|
|
1409
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1410
|
+
body: JSON.stringify(body),
|
|
1411
|
+
signal
|
|
1412
|
+
});
|
|
1413
|
+
if (response.ok)
|
|
1414
|
+
return (await response.json());
|
|
1415
|
+
if (response.status === 403)
|
|
1416
|
+
throw new AssetTypeForbiddenError('Asset type upsert forbidden');
|
|
1417
|
+
if (response.status === 409) {
|
|
1418
|
+
const current = (await response.json());
|
|
1419
|
+
throw new AssetTypeConflictError('Asset type was modified by another user', current);
|
|
1420
|
+
}
|
|
1421
|
+
if (response.status === 400) {
|
|
1422
|
+
const errorBody = await readErrorBody(response);
|
|
1423
|
+
throw new AssetTypeValidationError(`Asset type validation failed: ${errorBody.detail}`, errorBody);
|
|
1424
|
+
}
|
|
1425
|
+
const errorText = await response.text();
|
|
1426
|
+
throw new Error(`Failed to upsert asset type: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1427
|
+
}
|
|
1428
|
+
async deleteAssetType(categoryId, typeId, etag, signal) {
|
|
1429
|
+
return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ false, signal);
|
|
1430
|
+
}
|
|
1431
|
+
async restoreAssetType(categoryId, typeId, etag, signal) {
|
|
1432
|
+
return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ true, signal);
|
|
1433
|
+
}
|
|
1434
|
+
async _typeDeleteOrRestore(categoryId, typeId, etag, isRestore, signal) {
|
|
1435
|
+
const url = isRestore
|
|
1436
|
+
? `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/restore`
|
|
1437
|
+
: `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`;
|
|
1438
|
+
const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
|
|
1439
|
+
const response = await this._httpClient.fetch(method, url, {
|
|
1440
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1441
|
+
body: JSON.stringify({ etag }),
|
|
1442
|
+
signal
|
|
1443
|
+
});
|
|
1444
|
+
if (response.ok)
|
|
1445
|
+
return (await response.json());
|
|
1446
|
+
if (response.status === 403) {
|
|
1447
|
+
const op = isRestore ? 'restore' : 'delete';
|
|
1448
|
+
throw new AssetTypeForbiddenError(`Asset type ${op} forbidden`);
|
|
1449
|
+
}
|
|
1450
|
+
if (response.status === 409) {
|
|
1451
|
+
const current = (await response.json());
|
|
1452
|
+
throw new AssetTypeConflictError('Asset type was modified by another user', current);
|
|
1453
|
+
}
|
|
1454
|
+
if (response.status === 404)
|
|
1455
|
+
throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
|
|
1456
|
+
const errorText = await response.text();
|
|
1457
|
+
const op = isRestore ? 'restore' : 'delete';
|
|
1458
|
+
throw new Error(`Failed to ${op} asset type: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1459
|
+
}
|
|
1460
|
+
async getAssetTypeCatalog(signal) {
|
|
1461
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, this._assetTypeCatalogUrl, {
|
|
1462
|
+
headers: { Accept: 'application/json' },
|
|
1463
|
+
signal
|
|
1464
|
+
});
|
|
1465
|
+
if (response.ok)
|
|
1466
|
+
return (await response.json());
|
|
1467
|
+
if (response.status === 403)
|
|
1468
|
+
throw new AssetCategoryForbiddenError('Asset type catalog read forbidden');
|
|
1469
|
+
if (response.status === 400) {
|
|
1470
|
+
// The server emits a single structured 400 here today —
|
|
1471
|
+
// `cause: 'CatalogTooLarge'` — to flag a tenant whose catalog
|
|
1472
|
+
// overflows the per-customer cap. Surface as a typed error so
|
|
1473
|
+
// picker UIs can render "use search" instead of crashing on
|
|
1474
|
+
// missing rows. Any other 400 (malformed CatalogTooLarge body,
|
|
1475
|
+
// future cause) falls through to the generic error below so the
|
|
1476
|
+
// caller still sees the raw response.
|
|
1477
|
+
const errorBody = await readErrorBody(response);
|
|
1478
|
+
if (isAssetCatalogTooLargeErrorBody(errorBody)) {
|
|
1479
|
+
throw new AssetCatalogTooLargeError(`Asset type catalog too large: ${errorBody.detail}`, errorBody);
|
|
1480
|
+
}
|
|
1481
|
+
throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorBody.detail}`);
|
|
1482
|
+
}
|
|
1483
|
+
const errorText = await response.text();
|
|
1484
|
+
throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorText}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
//# sourceMappingURL=TwinClient.js.map
|