@hahnpro/hpc-api 2025.3.1 → 2025.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -22
- package/asset-synchronization.md +556 -0
- package/package.json +2 -2
- package/src/lib/interfaces/asset.interface.d.ts +1 -0
- package/src/lib/mock/api.mock.d.ts +3 -1
- package/src/lib/mock/api.mock.js +26 -0
- package/src/lib/mock/asset.mock.service.d.ts +3 -0
- package/src/lib/mock/asset.mock.service.js +9 -0
- package/src/lib/mock/content.mock.service.js +3 -2
- package/src/lib/mock/data.mock.service.js +4 -2
- package/src/lib/mock/http.mock.service.d.ts +3 -3
- package/src/lib/services/asset.service.d.ts +2 -0
- package/src/lib/services/asset.service.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @hahnpro/hpc-api
|
|
2
2
|
|
|
3
|
+
## 2025.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Added asset::findOneExternal and asset::updateOneExternal to reflect changes to asset API
|
|
8
|
+
- Updated some Mock Types to improve type-compliance between real and mock API classes
|
|
9
|
+
- Added documentation on how to develop an asset synchronization script from external systems
|
|
10
|
+
|
|
11
|
+
## 2025.3.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies to reduce vulnerabilities
|
|
16
|
+
|
|
3
17
|
## 2025.3.0
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ This example uses the [`pyjwt` library](https://pyjwt.readthedocs.io/en/stable/)
|
|
|
135
135
|
</details>
|
|
136
136
|
|
|
137
137
|
<details>
|
|
138
|
-
<summary markdown="span">
|
|
138
|
+
<summary markdown="span">Typescript</summary>
|
|
139
139
|
|
|
140
140
|
Get user roles from JWT-Token.
|
|
141
141
|
|
|
@@ -483,24 +483,3 @@ await api.timeSeries.addValue('1234', value);
|
|
|
483
483
|
```
|
|
484
484
|
|
|
485
485
|
</details>
|
|
486
|
-
|
|
487
|
-
## 4. FAQ
|
|
488
|
-
|
|
489
|
-
### 4.1. How to log messages from Python in a FlowFunction
|
|
490
|
-
|
|
491
|
-
> this is just possible for _RPC_ style Python integration.
|
|
492
|
-
|
|
493
|
-
In Typescript, you can register a listener for messages and `stderr` and pipe them to your `logger`.
|
|
494
|
-
|
|
495
|
-
```typescript
|
|
496
|
-
const script = this.runPyRpcScript(join(__dirname, 'my-awesome-script.py'));
|
|
497
|
-
script.addListener('stderr', (data) => this.logger.error('py: ' + data));
|
|
498
|
-
script.on('message', (data) => this.logger.debug('py: ' + data));
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
Then you can simply use print in python for logging
|
|
502
|
-
|
|
503
|
-
```python
|
|
504
|
-
print("log the logging log")
|
|
505
|
-
print("this is an error", file=sys.stderr)
|
|
506
|
-
```
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# Asset Synchronization Documentation
|
|
2
|
+
|
|
3
|
+
This documentation describes the process for synchronizing assets and asset types with the Hahn PRO Cloud API, including synchronization using external keys.
|
|
4
|
+
|
|
5
|
+
The fundamental approaches are useful in any language, but the examples here are in typescript. At the end you will have a
|
|
6
|
+
complete script for synchronizing Assettypes and Assets, i.e. creating them if they don´t exist and updating existing ones.
|
|
7
|
+
There are multiple approaches shown here, select the one that fits your use-case.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The synchronization process follows two main steps:
|
|
12
|
+
|
|
13
|
+
1. **Synchronize Asset Types**: Check and create asset types based on their names
|
|
14
|
+
2. **Synchronize Assets**: Check and create assets based on their names or external keys using a similar process
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Access to Hahn PRO Cloud API
|
|
19
|
+
- Valid authentication (JWT token)
|
|
20
|
+
- Permissions to create asset types and assets
|
|
21
|
+
|
|
22
|
+
## Synchronization Methods
|
|
23
|
+
|
|
24
|
+
There are two approaches for asset synchronization:
|
|
25
|
+
|
|
26
|
+
1. **Name-based synchronization**: Uses the asset name for duplicate detection
|
|
27
|
+
2. **External key-based synchronization**: Uses the `externalKey` field for duplicate detection
|
|
28
|
+
3. **Update mode**: Updates existing assets instead of skipping them
|
|
29
|
+
|
|
30
|
+
## 1. Asset Type Synchronization
|
|
31
|
+
|
|
32
|
+
### Process
|
|
33
|
+
|
|
34
|
+
1. **Fetch existing asset types**: Load all current asset types
|
|
35
|
+
2. **Name comparison**: For each asset type to be created, check if a type with the same name already exists
|
|
36
|
+
3. **Check dependencies**: Ensure all parent types already exist
|
|
37
|
+
4. **Create**: Create new asset types if they don't exist
|
|
38
|
+
|
|
39
|
+
### TypeScript Implementation
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
async function synchronizeAssetTypes(assetTypes: AssetType[], orgId: string): Promise<void> {
|
|
43
|
+
// Load all existing asset types
|
|
44
|
+
const existingTypes = await api.assetTypes.getMany();
|
|
45
|
+
const existingTypeNames = new Set(existingTypes.docs.map((type) => type.name));
|
|
46
|
+
|
|
47
|
+
// Map for name-to-ID mapping
|
|
48
|
+
const assetTypeNameIdMap = new Map<string, string>();
|
|
49
|
+
|
|
50
|
+
// Populate map with existing types
|
|
51
|
+
existingTypes.docs.forEach((type) => {
|
|
52
|
+
assetTypeNameIdMap.set(type.name, type.id);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Process asset types (considering dependencies)
|
|
56
|
+
while (assetTypes.length > 0) {
|
|
57
|
+
const type = assetTypes.shift();
|
|
58
|
+
|
|
59
|
+
// Check if type already exists
|
|
60
|
+
if (existingTypeNames.has(type.name)) {
|
|
61
|
+
console.log(`Asset type '${type.name}' already exists - skipping`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if all parent types have been created
|
|
66
|
+
if (!(type.allowedParents || [])?.every((parent) => assetTypeNameIdMap.has(parent))) {
|
|
67
|
+
// Not all parent types exist -> requeue
|
|
68
|
+
assetTypes.push(type);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Create new asset type
|
|
74
|
+
const created = await api.assetTypes.addOne({
|
|
75
|
+
...type,
|
|
76
|
+
allowedParents: (type.allowedParents || []).map((parent) => assetTypeNameIdMap.get(parent)),
|
|
77
|
+
readWritePermissions: [...type.readWritePermissions, orgId],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assetTypeNameIdMap.set(type.name, created.id);
|
|
81
|
+
console.log(`Asset type '${type.name}' created successfully`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Error creating asset type '${type.name}':`, error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 2. Asset Synchronization by Name (Create Only)
|
|
91
|
+
|
|
92
|
+
### Process
|
|
93
|
+
|
|
94
|
+
1. **Fetch existing assets**: Load all current assets
|
|
95
|
+
2. **Name comparison**: For each asset to be created, check if an asset with the same name already exists
|
|
96
|
+
3. **Check parent dependencies**: Ensure parent assets already exist
|
|
97
|
+
4. **Type mapping**: Map asset type based on name
|
|
98
|
+
5. **Create**: Create new assets if they don't exist
|
|
99
|
+
|
|
100
|
+
### TypeScript Implementation
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
async function synchronizeAssetsByName(assets: Asset[], orgId: string, updateExisting: boolean = false): Promise<void> {
|
|
104
|
+
// Load all existing assets
|
|
105
|
+
const existingAssets = await api.assets.getMany();
|
|
106
|
+
const existingAssetNames = new Map<string, string>(); // name -> id
|
|
107
|
+
|
|
108
|
+
// Map for name-to-ID mapping
|
|
109
|
+
const assetNameIdMap = new Map<string, string>();
|
|
110
|
+
|
|
111
|
+
// Populate map with existing assets
|
|
112
|
+
existingAssets.docs.forEach((asset) => {
|
|
113
|
+
assetNameIdMap.set(asset.name, asset.id);
|
|
114
|
+
existingAssetNames.set(asset.name, asset.id);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Process assets (considering parent dependencies)
|
|
118
|
+
while (assets.length > 0) {
|
|
119
|
+
const asset = assets.shift();
|
|
120
|
+
|
|
121
|
+
// Check if asset already exists
|
|
122
|
+
if (existingAssetNames.has(asset.name)) {
|
|
123
|
+
if (updateExisting) {
|
|
124
|
+
try {
|
|
125
|
+
// Update existing asset
|
|
126
|
+
const existingId = existingAssetNames.get(asset.name);
|
|
127
|
+
const updated = await api.assets.updateOne(existingId, {
|
|
128
|
+
...asset,
|
|
129
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
130
|
+
parent: assetNameIdMap.get(asset.parent),
|
|
131
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
132
|
+
});
|
|
133
|
+
console.log(`Updated existing asset '${asset.name}'`);
|
|
134
|
+
assetNameIdMap.set(asset.name, updated.id);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`Error updating asset '${asset.name}':`, error);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`Asset '${asset.name}' already exists - skipping`);
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check if parent asset exists (if defined)
|
|
146
|
+
if (asset.parent && !assetNameIdMap.has(asset.parent)) {
|
|
147
|
+
// Parent doesn't exist yet -> requeue
|
|
148
|
+
assets.push(asset);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if asset type exists
|
|
153
|
+
if (!assetTypeNameIdMap.has(asset.type as string)) {
|
|
154
|
+
throw new Error(`Asset type '${asset.type}' not found`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Create new asset
|
|
159
|
+
const created = await api.assets.addOne({
|
|
160
|
+
...asset,
|
|
161
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
162
|
+
parent: assetNameIdMap.get(asset.parent),
|
|
163
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assetNameIdMap.set(asset.name, created.id);
|
|
167
|
+
console.log(`Asset '${asset.name}' created successfully`);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`Error creating asset '${asset.name}':`, error);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 3. Asset Synchronization by External Key
|
|
177
|
+
|
|
178
|
+
When synchronizing assets from external systems, you can use the `externalKey` field to identify assets. This is particularly useful when the external system has its own unique identifiers.
|
|
179
|
+
|
|
180
|
+
### Process
|
|
181
|
+
|
|
182
|
+
1. **Check for existing assets by external key**: Use the external endpoints to find existing assets
|
|
183
|
+
2. **Update or create**: Update existing assets or create new ones based on external key presence
|
|
184
|
+
3. **Handle dependencies**: Ensure parent assets exist before creating child assets
|
|
185
|
+
|
|
186
|
+
### TypeScript Implementation
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
async function synchronizeAssetsByExternalKey(assets: Asset[], orgId: string): Promise<void> {
|
|
190
|
+
// Maps for tracking assets
|
|
191
|
+
const assetExternalKeyIdMap = new Map<string, string>();
|
|
192
|
+
const assetNameIdMap = new Map<string, string>();
|
|
193
|
+
|
|
194
|
+
// Process assets (considering parent dependencies)
|
|
195
|
+
while (assets.length > 0) {
|
|
196
|
+
const asset = assets.shift();
|
|
197
|
+
|
|
198
|
+
if (!asset.externalKey) {
|
|
199
|
+
throw new Error(`Asset '${asset.name}' missing externalKey for external synchronization`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if parent asset exists (if defined)
|
|
203
|
+
if (asset.parent && !assetNameIdMap.has(asset.parent) && !assetExternalKeyIdMap.has(asset.parent)) {
|
|
204
|
+
// Parent doesn't exist yet -> requeue
|
|
205
|
+
assets.push(asset);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if asset type exists
|
|
210
|
+
if (!assetTypeNameIdMap.has(asset.type as string)) {
|
|
211
|
+
throw new Error(`Asset type '${asset.type}' not found`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Try to find existing asset by external key
|
|
216
|
+
let existingAsset: Asset | null = null;
|
|
217
|
+
try {
|
|
218
|
+
existingAsset = await api.assets.findOneExternal(asset.externalKey);
|
|
219
|
+
console.log(`Found existing asset with external key '${asset.externalKey}'`);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// Asset doesn't exist yet - this is expected for new assets
|
|
222
|
+
console.log(`Asset with external key '${asset.externalKey}' not found - will create new`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let resultAsset: Asset;
|
|
226
|
+
|
|
227
|
+
if (existingAsset) {
|
|
228
|
+
// Update existing asset using external key endpoint
|
|
229
|
+
resultAsset = await api.assets.updateOneExternal(asset.externalKey, {
|
|
230
|
+
...asset,
|
|
231
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
232
|
+
parent: assetNameIdMap.get(asset.parent) || assetExternalKeyIdMap.get(asset.parent),
|
|
233
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
234
|
+
});
|
|
235
|
+
console.log(`Updated asset with external key '${asset.externalKey}'`);
|
|
236
|
+
} else {
|
|
237
|
+
// Create new asset
|
|
238
|
+
resultAsset = await api.assets.addOne({
|
|
239
|
+
...asset,
|
|
240
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
241
|
+
parent: assetNameIdMap.get(asset.parent) || assetExternalKeyIdMap.get(asset.parent),
|
|
242
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
243
|
+
});
|
|
244
|
+
console.log(`Created new asset with external key '${asset.externalKey}'`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Update maps for dependency resolution
|
|
248
|
+
assetExternalKeyIdMap.set(asset.externalKey, resultAsset.id);
|
|
249
|
+
assetNameIdMap.set(asset.name, resultAsset.id);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(`Error processing asset with external key '${asset.externalKey}':`, error);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 4. Mixed Synchronization (Name and External Key)
|
|
259
|
+
|
|
260
|
+
For scenarios where some assets have external keys and others don't, you can use a mixed approach:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
async function synchronizeAssetsMixed(assets: Asset[], orgId: string, updateExisting: boolean = false): Promise<void> {
|
|
264
|
+
// Load all existing assets for name-based lookup
|
|
265
|
+
const existingAssets = await api.assets.getMany();
|
|
266
|
+
const existingAssetNames = new Map<string, string>(); // name -> id
|
|
267
|
+
|
|
268
|
+
// Maps for tracking assets
|
|
269
|
+
const assetExternalKeyIdMap = new Map<string, string>();
|
|
270
|
+
const assetNameIdMap = new Map<string, string>();
|
|
271
|
+
|
|
272
|
+
// Populate map with existing assets
|
|
273
|
+
existingAssets.docs.forEach((asset) => {
|
|
274
|
+
assetNameIdMap.set(asset.name, asset.id);
|
|
275
|
+
existingAssetNames.set(asset.name, asset.id);
|
|
276
|
+
if (asset.externalKey) {
|
|
277
|
+
assetExternalKeyIdMap.set(asset.externalKey, asset.id);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Process assets (considering parent dependencies)
|
|
282
|
+
while (assets.length > 0) {
|
|
283
|
+
const asset = assets.shift();
|
|
284
|
+
|
|
285
|
+
// Determine identification method
|
|
286
|
+
const useExternalKey = !!asset.externalKey;
|
|
287
|
+
const identifier = useExternalKey ? asset.externalKey : asset.name;
|
|
288
|
+
|
|
289
|
+
// Check if parent asset exists (if defined)
|
|
290
|
+
if (asset.parent && !assetNameIdMap.has(asset.parent) && !assetExternalKeyIdMap.has(asset.parent)) {
|
|
291
|
+
// Parent doesn't exist yet -> requeue
|
|
292
|
+
assets.push(asset);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check if asset type exists
|
|
297
|
+
if (!assetTypeNameIdMap.has(asset.type as string)) {
|
|
298
|
+
throw new Error(`Asset type '${asset.type}' not found`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
let resultAsset: Asset;
|
|
303
|
+
let assetExists = false;
|
|
304
|
+
|
|
305
|
+
if (useExternalKey) {
|
|
306
|
+
// Try to find existing asset by external key
|
|
307
|
+
try {
|
|
308
|
+
await api.assets.findOneExternal(asset.externalKey);
|
|
309
|
+
assetExists = true;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
assetExists = false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (assetExists) {
|
|
315
|
+
// Update existing asset using external key endpoint
|
|
316
|
+
resultAsset = await api.assets.updateOneExternal(asset.externalKey, {
|
|
317
|
+
...asset,
|
|
318
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
319
|
+
parent: assetNameIdMap.get(asset.parent) || assetExternalKeyIdMap.get(asset.parent),
|
|
320
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
321
|
+
});
|
|
322
|
+
console.log(`Updated asset with external key '${asset.externalKey}'`);
|
|
323
|
+
} else {
|
|
324
|
+
// Create new asset
|
|
325
|
+
resultAsset = await api.assets.addOne({
|
|
326
|
+
...asset,
|
|
327
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
328
|
+
parent: assetNameIdMap.get(asset.parent) || assetExternalKeyIdMap.get(asset.parent),
|
|
329
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
330
|
+
});
|
|
331
|
+
console.log(`Created new asset with external key '${asset.externalKey}'`);
|
|
332
|
+
}
|
|
333
|
+
assetExternalKeyIdMap.set(asset.externalKey, resultAsset.id);
|
|
334
|
+
} else {
|
|
335
|
+
// Name-based approach
|
|
336
|
+
assetExists = existingAssetNames.has(asset.name);
|
|
337
|
+
|
|
338
|
+
if (assetExists && updateExisting) {
|
|
339
|
+
// Update existing asset using standard endpoint
|
|
340
|
+
const existingId = existingAssetNames.get(asset.name);
|
|
341
|
+
resultAsset = await api.assets.updateOne(existingId, {
|
|
342
|
+
...asset,
|
|
343
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
344
|
+
parent: assetNameIdMap.get(asset.parent) || assetExternalKeyIdMap.get(asset.parent),
|
|
345
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
346
|
+
});
|
|
347
|
+
console.log(`Updated existing asset '${asset.name}'`);
|
|
348
|
+
} else if (assetExists && !updateExisting) {
|
|
349
|
+
console.log(`Asset '${asset.name}' already exists - skipping`);
|
|
350
|
+
continue;
|
|
351
|
+
} else {
|
|
352
|
+
// Create new asset using standard method
|
|
353
|
+
resultAsset = await api.assets.addOne({
|
|
354
|
+
...asset,
|
|
355
|
+
type: assetTypeNameIdMap.get(asset.type as string),
|
|
356
|
+
parent: assetNameIdMap.get(asset.parent) || assetExternalKeyIdMap.get(asset.parent),
|
|
357
|
+
readWritePermissions: [...asset.readWritePermissions, orgId],
|
|
358
|
+
});
|
|
359
|
+
console.log(`Created asset '${asset.name}'`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
assetNameIdMap.set(asset.name, resultAsset.id);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error(`Error processing asset '${identifier}':`, error);
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## 5. Complete Synchronization Function
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
interface SynchronizationOptions {
|
|
376
|
+
useExternalKeys?: boolean;
|
|
377
|
+
updateExisting?: boolean;
|
|
378
|
+
mixedMode?: boolean;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function synchronizeAssetsComplete(
|
|
382
|
+
assetTypes: AssetType[],
|
|
383
|
+
assets: Asset[],
|
|
384
|
+
orgId: string,
|
|
385
|
+
options: SynchronizationOptions = {},
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
const { useExternalKeys = false, updateExisting = false, mixedMode = false } = options;
|
|
388
|
+
|
|
389
|
+
console.log('Starting asset synchronization...');
|
|
390
|
+
console.log(`Options: External Keys: ${useExternalKeys}, Update Existing: ${updateExisting}, Mixed Mode: ${mixedMode}`);
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
// Step 1: Synchronize asset types
|
|
394
|
+
console.log('Synchronizing asset types...');
|
|
395
|
+
await synchronizeAssetTypes([...assetTypes], orgId);
|
|
396
|
+
|
|
397
|
+
// Step 2: Synchronize assets
|
|
398
|
+
console.log('Synchronizing assets...');
|
|
399
|
+
if (mixedMode) {
|
|
400
|
+
await synchronizeAssetsMixed([...assets], orgId, updateExisting);
|
|
401
|
+
} else if (useExternalKeys) {
|
|
402
|
+
await synchronizeAssetsByExternalKey([...assets], orgId);
|
|
403
|
+
} else {
|
|
404
|
+
await synchronizeAssetsByName([...assets], orgId, updateExisting);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log('Asset synchronization completed successfully!');
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('Error during asset synchronization:', error);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## 6. Usage Examples
|
|
416
|
+
|
|
417
|
+
### Create Only (Skip Existing)
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { readFileSync } from 'fs';
|
|
421
|
+
|
|
422
|
+
import { API, Asset, AssetType } from '@hahnpro/hpc-api';
|
|
423
|
+
|
|
424
|
+
// Initialize API
|
|
425
|
+
const api = new API();
|
|
426
|
+
|
|
427
|
+
// Load data from JSON files
|
|
428
|
+
const assetTypes = JSON.parse(readFileSync('assetTypes.json', 'utf-8')) as AssetType[];
|
|
429
|
+
const assets = JSON.parse(readFileSync('assets.json', 'utf-8')) as Asset[];
|
|
430
|
+
|
|
431
|
+
// Organization ID
|
|
432
|
+
const orgId = 'your-organization-id';
|
|
433
|
+
|
|
434
|
+
// Execute synchronization - create only
|
|
435
|
+
await synchronizeAssetsComplete(assetTypes, assets, orgId, {
|
|
436
|
+
updateExisting: false,
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Update Existing Assets by Name
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// Execute synchronization - update existing assets by name
|
|
444
|
+
await synchronizeAssetsComplete(assetTypes, assets, orgId, {
|
|
445
|
+
updateExisting: true,
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Using External Key-based Synchronization
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// Execute synchronization using external keys (always updates existing)
|
|
453
|
+
await synchronizeAssetsComplete(assetTypes, assets, orgId, {
|
|
454
|
+
useExternalKeys: true,
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Mixed Mode with Updates
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// Execute mixed mode synchronization with updates
|
|
462
|
+
await synchronizeAssetsComplete(assetTypes, assets, orgId, {
|
|
463
|
+
mixedMode: true,
|
|
464
|
+
updateExisting: true,
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Data Format
|
|
469
|
+
|
|
470
|
+
### Asset Type JSON Structure
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
[
|
|
474
|
+
{
|
|
475
|
+
"name": "Building",
|
|
476
|
+
"allowedParents": [],
|
|
477
|
+
"readPermissions": [],
|
|
478
|
+
"readWritePermissions": ["some-role"],
|
|
479
|
+
"typeSchema": {
|
|
480
|
+
"type": "object",
|
|
481
|
+
"properties": {
|
|
482
|
+
"example": {
|
|
483
|
+
"type": "string",
|
|
484
|
+
"title": "example"
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Asset JSON Structure (Name-based)
|
|
493
|
+
|
|
494
|
+
```json
|
|
495
|
+
[
|
|
496
|
+
{
|
|
497
|
+
"name": "Main Building",
|
|
498
|
+
"type": "Building",
|
|
499
|
+
"readPermissions": [],
|
|
500
|
+
"readWritePermissions": ["some_role"],
|
|
501
|
+
"data": {
|
|
502
|
+
"example": "foo"
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Asset JSON Structure (External Key-based)
|
|
509
|
+
|
|
510
|
+
```json
|
|
511
|
+
[
|
|
512
|
+
{
|
|
513
|
+
"name": "Main Building",
|
|
514
|
+
"externalKey": "EXT-BUILDING-001",
|
|
515
|
+
"type": "Building",
|
|
516
|
+
"parent": "EXT-SITE-001",
|
|
517
|
+
"readPermissions": [],
|
|
518
|
+
"readWritePermissions": ["some_role"],
|
|
519
|
+
"data": {
|
|
520
|
+
"example": "foo"
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Important Notes
|
|
527
|
+
|
|
528
|
+
- **Update Behavior**: When `updateExisting` is true, the entire asset object is replaced with the new data
|
|
529
|
+
- **External Key Updates**: External key synchronization always attempts to update existing assets first
|
|
530
|
+
- **Dependency Resolution**: Parent references can be either names or external keys, depending on the synchronization method
|
|
531
|
+
- **Error Handling**: The system handles both creation and update scenarios gracefully
|
|
532
|
+
- **Performance**: External key synchronization may be slower due to individual lookups for each asset
|
|
533
|
+
- **Mixed Mode**: Allows combination of name-based and external key-based assets in the same synchronization run
|
|
534
|
+
|
|
535
|
+
## API Endpoints Used
|
|
536
|
+
|
|
537
|
+
- **Standard Asset Operations**:
|
|
538
|
+
- - Get all assets `GET /api/assets`
|
|
539
|
+
- - Create new asset `POST /api/assets`
|
|
540
|
+
- - Update existing asset by ID `PUT /api/assets/{id}`
|
|
541
|
+
|
|
542
|
+
- **External Key Operations**:
|
|
543
|
+
- - Get asset by external key `GET /api/assets/external/{key}`
|
|
544
|
+
- - Update asset by external key `PUT /api/assets/external/{key}`
|
|
545
|
+
|
|
546
|
+
## Limitations
|
|
547
|
+
|
|
548
|
+
The example Code above is not an all-encompassing solution for synchronization. It has some limitations:
|
|
549
|
+
|
|
550
|
+
### Partial state if errors occur
|
|
551
|
+
|
|
552
|
+
If some error where to occur while synchronizing, a partiality-synced state will be left behind.
|
|
553
|
+
|
|
554
|
+
### Only Assettypes and Assets
|
|
555
|
+
|
|
556
|
+
Only these two Datatypes are synchronized. A Synchronization of other data can be done with a similar approach like shown here.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hahnpro/hpc-api",
|
|
3
|
-
"version": "2025.3.
|
|
3
|
+
"version": "2025.3.2",
|
|
4
4
|
"description": "Module for easy access to the HahnPRO Cloud API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"url": "https://hahnpro.com"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"axios": "1.
|
|
11
|
+
"axios": "1.11.0",
|
|
12
12
|
"eventsource": "4.0.0",
|
|
13
13
|
"form-data": "4.0.4",
|
|
14
14
|
"jose": "5.10.0",
|
|
@@ -15,6 +15,7 @@ import { Label } from '../label.interface';
|
|
|
15
15
|
import { NotificationRuleService } from '../notification-rule.service';
|
|
16
16
|
import { Notification } from '../notification.interface';
|
|
17
17
|
import { Organization } from '../organization.interface';
|
|
18
|
+
import { ProxyService } from '../proxy.service';
|
|
18
19
|
import { Secret } from '../secret.interface';
|
|
19
20
|
import { AiService } from '../services';
|
|
20
21
|
import { Artifact } from '../storage.interface';
|
|
@@ -71,7 +72,7 @@ export declare class MockAPI implements API {
|
|
|
71
72
|
flowModules: FlowModuleService;
|
|
72
73
|
labels: LabelMockService;
|
|
73
74
|
notificationRules: NotificationRuleService;
|
|
74
|
-
proxy:
|
|
75
|
+
proxy: ProxyService;
|
|
75
76
|
secrets: SecretMockService;
|
|
76
77
|
tasks: TaskMockService;
|
|
77
78
|
timeSeries: TimeseriesMockService;
|
|
@@ -80,6 +81,7 @@ export declare class MockAPI implements API {
|
|
|
80
81
|
notifications: NotificationMockService;
|
|
81
82
|
organizations: OrganizationMockService;
|
|
82
83
|
constructor(initData: MockAPIInitData);
|
|
84
|
+
getEverything(): Promise<{}>;
|
|
83
85
|
}
|
|
84
86
|
export type Identity<T> = {
|
|
85
87
|
[P in keyof T]: T[P];
|
package/src/lib/mock/api.mock.js
CHANGED
|
@@ -206,5 +206,31 @@ class MockAPI {
|
|
|
206
206
|
this.organizations = new organization_mock_service_1.OrganizationMockService(organizations1);
|
|
207
207
|
this.httpClient = new http_mock_service_1.HttpMockService();
|
|
208
208
|
}
|
|
209
|
+
async getEverything() {
|
|
210
|
+
const results = {
|
|
211
|
+
assets: await this.assets.getMany(),
|
|
212
|
+
assetTypes: await this.assetTypes.getMany(),
|
|
213
|
+
contents: await this.contents.getMany(),
|
|
214
|
+
endpoints: await this.endpoints.getMany(),
|
|
215
|
+
secrets: await this.secrets.getMany(),
|
|
216
|
+
timeSeries: await this.timeSeries.getMany(),
|
|
217
|
+
tasks: await this.tasks.getMany(),
|
|
218
|
+
events: await this.events.getMany(),
|
|
219
|
+
flows: await this.flows.getMany(),
|
|
220
|
+
flowDeployments: await this.flowDeployments.getMany(),
|
|
221
|
+
flowFunctions: await this.flowFunctions.getMany(),
|
|
222
|
+
flowModules: await this.flowModules.getMany(),
|
|
223
|
+
labels: await this.labels.getMany(),
|
|
224
|
+
vault: await this.vault.getMany(),
|
|
225
|
+
notifications: await this.notifications.getMany(),
|
|
226
|
+
organizations: await this.organizations.getMany(),
|
|
227
|
+
};
|
|
228
|
+
return Object.keys(results).reduce((previousValue, currentValue) => {
|
|
229
|
+
if (results[currentValue].total > 0) {
|
|
230
|
+
previousValue[currentValue] = results[currentValue];
|
|
231
|
+
}
|
|
232
|
+
return previousValue;
|
|
233
|
+
}, {});
|
|
234
|
+
}
|
|
209
235
|
}
|
|
210
236
|
exports.MockAPI = MockAPI;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import FormData from 'form-data';
|
|
2
2
|
import { Paginated, RequestParameter } from '../data.interface';
|
|
3
|
+
import { TokenOption } from '../http.service';
|
|
3
4
|
import { Asset, AssetRevision, Attachment, EventCause, EventLevelOverride } from '../interfaces';
|
|
4
5
|
import { AssetService } from '../services';
|
|
5
6
|
import { APIBaseMock } from './api-base.mock';
|
|
@@ -27,5 +28,7 @@ export declare class AssetMockService extends BaseService implements AssetServic
|
|
|
27
28
|
getRevisions(assetId: string): Promise<Paginated<AssetRevision[]>>;
|
|
28
29
|
rollback(assetId: string, revisionId: string): Promise<Asset>;
|
|
29
30
|
deleteRevision(assetId: string, revisionId: string): Promise<any>;
|
|
31
|
+
findOneExternal(key: string, options?: TokenOption): Promise<Asset>;
|
|
32
|
+
updateOneExternal(key: string, dto: any, options?: TokenOption): Promise<Asset>;
|
|
30
33
|
}
|
|
31
34
|
export {};
|
|
@@ -111,5 +111,14 @@ class AssetMockService extends BaseService {
|
|
|
111
111
|
this.revisions.splice(index, 1);
|
|
112
112
|
return Promise.resolve(revisionId);
|
|
113
113
|
}
|
|
114
|
+
findOneExternal(key, options) {
|
|
115
|
+
return Promise.resolve(this.data.find((asset) => asset.externalKey === key));
|
|
116
|
+
}
|
|
117
|
+
async updateOneExternal(key, dto, options) {
|
|
118
|
+
const asset = await this.findOneExternal(key, options);
|
|
119
|
+
const index = this.data.findIndex((asset) => asset.externalKey === key);
|
|
120
|
+
this.data[index] = { ...asset, ...dto };
|
|
121
|
+
return Promise.resolve(this.data[index]);
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
exports.AssetMockService = AssetMockService;
|
|
@@ -78,8 +78,9 @@ class ContentMockService extends BaseService {
|
|
|
78
78
|
const page = this.getItems(params, false);
|
|
79
79
|
return Promise.resolve(page);
|
|
80
80
|
}
|
|
81
|
-
upload(form) {
|
|
82
|
-
|
|
81
|
+
async upload(form) {
|
|
82
|
+
const content = await this.addOne({});
|
|
83
|
+
return Promise.resolve(content);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
exports.ContentMockService = ContentMockService;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DataMockService = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
4
5
|
const data_interface_1 = require("../data.interface");
|
|
5
6
|
const data_service_1 = require("../data.service");
|
|
6
7
|
class DataMockService extends data_service_1.DataService {
|
|
@@ -13,8 +14,9 @@ class DataMockService extends data_service_1.DataService {
|
|
|
13
14
|
return Promise.all(map);
|
|
14
15
|
}
|
|
15
16
|
addOne(dto) {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const dtoWithId = { id: (0, crypto_1.randomUUID)(), ...dto };
|
|
18
|
+
this.data.push(dtoWithId);
|
|
19
|
+
return Promise.resolve(dtoWithId);
|
|
18
20
|
}
|
|
19
21
|
deleteOne(id) {
|
|
20
22
|
const index = this.data.findIndex((v) => v.id === id);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Method } from 'axios';
|
|
1
|
+
import { AxiosInstance, Method } from 'axios';
|
|
2
2
|
import { Config, HttpClient, Issuer, TokenOption } from '../http.service';
|
|
3
3
|
import { TokenSet } from '../token-set';
|
|
4
4
|
export declare class HttpMockService extends HttpClient {
|
|
5
|
-
protected readonly authAxiosInstance:
|
|
6
|
-
protected readonly axiosInstance:
|
|
5
|
+
protected readonly authAxiosInstance: AxiosInstance;
|
|
6
|
+
protected readonly axiosInstance: AxiosInstance;
|
|
7
7
|
constructor();
|
|
8
8
|
delete: <T>(_url: string, _config: Config | undefined) => Promise<T>;
|
|
9
9
|
get: <T>(_url: string, _config: Config | undefined) => Promise<T>;
|
|
@@ -20,5 +20,7 @@ export declare class AssetService extends BaseService {
|
|
|
20
20
|
getRevisions(assetId: string, options?: TokenOption): Promise<Paginated<AssetRevision[]>>;
|
|
21
21
|
rollback(assetId: string, revisionId: string, options?: TokenOption): Promise<Asset>;
|
|
22
22
|
deleteRevision(assetId: string, revisionId: string, options?: TokenOption): Promise<any>;
|
|
23
|
+
findOneExternal(key: string, options?: TokenOption): Promise<Asset>;
|
|
24
|
+
updateOneExternal(key: string, dto: any, options?: TokenOption): Promise<Asset>;
|
|
23
25
|
}
|
|
24
26
|
export {};
|
|
@@ -48,5 +48,11 @@ class AssetService extends BaseService {
|
|
|
48
48
|
deleteRevision(assetId, revisionId, options = {}) {
|
|
49
49
|
return this.httpClient.delete(`${this.basePath}/${assetId}/revisions/${revisionId}`, options);
|
|
50
50
|
}
|
|
51
|
+
findOneExternal(key, options = {}) {
|
|
52
|
+
return this.httpClient.get(`${this.basePath}/external/${key}`, options);
|
|
53
|
+
}
|
|
54
|
+
updateOneExternal(key, dto, options = {}) {
|
|
55
|
+
return this.httpClient.put(`${this.basePath}/external/${key}`, dto, options);
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
exports.AssetService = AssetService;
|