Skip to content

Commit 37bd6c4

Browse files
feat(assets): add updateValueById for PUT /odata/Assets({key})
Adds Assets.updateValueById(id, folderId, newValue) that fetches the asset to determine its valueType, validates the newValue runtime type matches (Text/Integer/Bool only), and PUTs the update while preserving the asset's name, scope, and description. Unsupported valueTypes (Credential, Secret) throw ValidationError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 887631a commit 37bd6c4

7 files changed

Lines changed: 415 additions & 8 deletions

File tree

docs/oauth-scopes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This page lists the specific OAuth scopes required in external app for each SDK
99
| `getAll()` | `OR.Assets` or `OR.Assets.Read` |
1010
| `getById()` | `OR.Assets` or `OR.Assets.Read` |
1111
| `getByName()` | `OR.Assets` or `OR.Assets.Read` |
12+
| `updateValueById()` | `OR.Assets` or `OR.Assets.Write` |
1213

1314
## Jobs
1415

src/models/orchestrator/assets.models.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AssetGetAllOptions, AssetGetResponse, AssetGetByIdOptions, AssetGetByNameOptions } from './assets.types';
1+
import { AssetGetAllOptions, AssetGetResponse, AssetGetByIdOptions, AssetGetByNameOptions, AssetNewValue, AssetUpdateValueByIdOptions } from './assets.types';
22
import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination';
33

44
/**
@@ -90,4 +90,43 @@ export interface AssetServiceModel {
9090
* ```
9191
*/
9292
getByName(name: string, options?: AssetGetByNameOptions): Promise<AssetGetResponse>;
93+
94+
/**
95+
* Updates the value of an existing asset by ID.
96+
*
97+
* Fetches the asset internally to determine its type, then updates only the value while
98+
* preserving the asset's name, scope, and description.
99+
*
100+
* **Supported value types:** `Text`, `Integer`, and `Bool` only. Other types
101+
* (`Credential`, `Secret`) throw a `ValidationError`.
102+
*
103+
* The `newValue` runtime type must match the asset's `valueType`:
104+
* - `Text` → `string`
105+
* - `Integer` → `number` (integer)
106+
* - `Bool` → `boolean`
107+
*
108+
* A `ValidationError` is thrown when the `newValue` type does not match.
109+
*
110+
* Returns void; the API does not return the updated asset, so call
111+
* {@link AssetServiceModel.getById} or {@link AssetServiceModel.getByName}
112+
* if you need the refreshed value.
113+
*
114+
* @param id - Asset ID
115+
* @param newValue - New value to apply (string for `Text`, number for `Integer`, boolean for `Bool`)
116+
* @param options - Folder scoping (`folderId` / `folderKey` / `folderPath`)
117+
* @returns Promise resolving when the asset has been updated
118+
*
119+
* @example
120+
* ```typescript
121+
* // Update a Text asset by folder ID
122+
* await assets.updateValueById(<assetId>, 'new-value', { folderId: <folderId> });
123+
*
124+
* // Update an Integer asset by folder key (GUID)
125+
* await assets.updateValueById(<assetId>, 42, { folderKey: '5f6dadf1-3677-49dc-8aca-c2999dd4b3ba' });
126+
*
127+
* // Update a Bool asset by folder path
128+
* await assets.updateValueById(<assetId>, true, { folderPath: 'Shared/Finance' });
129+
* ```
130+
*/
131+
updateValueById(id: number, newValue: AssetNewValue, options?: AssetUpdateValueByIdOptions): Promise<void>;
93132
}

src/models/orchestrator/assets.types.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,10 @@ export enum AssetValueScope {
1313
* Enum for Asset Value Type
1414
*/
1515
export enum AssetValueType {
16-
DBConnectionString = 'DBConnectionString',
17-
HttpConnectionString = 'HttpConnectionString',
1816
Text = 'Text',
1917
Bool = 'Bool',
2018
Integer = 'Integer',
2119
Credential = 'Credential',
22-
WindowsCredential = 'WindowsCredential',
23-
KeyValueList = 'KeyValueList',
2420
Secret = 'Secret'
2521
}
2622

@@ -73,3 +69,18 @@ export interface AssetGetByIdOptions extends BaseOptions {}
7369
* Options for getting a single asset by name
7470
*/
7571
export interface AssetGetByNameOptions extends FolderScopedOptions {}
72+
73+
/**
74+
* New value accepted by {@link AssetServiceModel.updateValueById}.
75+
*
76+
* The runtime type must match the asset's `valueType`:
77+
* - `Text` → `string`
78+
* - `Integer` → `number`
79+
* - `Bool` → `boolean`
80+
*/
81+
export type AssetNewValue = string | number | boolean;
82+
83+
/**
84+
* Options for updating an asset value by ID
85+
*/
86+
export interface AssetUpdateValueByIdOptions extends FolderScopedOptions {}

src/services/orchestrator/assets/assets.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { FolderScopedService } from '../../folder-scoped';
2-
import { AssetGetResponse, AssetGetAllOptions, AssetGetByIdOptions, AssetGetByNameOptions } from '../../../models/orchestrator/assets.types';
2+
import { AssetGetResponse, AssetGetAllOptions, AssetGetByIdOptions, AssetGetByNameOptions, AssetNewValue, AssetUpdateValueByIdOptions, AssetValueScope, AssetValueType } from '../../../models/orchestrator/assets.types';
33
import { AssetServiceModel } from '../../../models/orchestrator/assets.models';
44
import { addPrefixToKeys, pascalToCamelCaseKeys, transformData } from '../../../utils/transform';
55
import { createHeaders } from '../../../utils/http/headers';
66
import { FOLDER_ID } from '../../../utils/constants/headers';
7+
import { resolveFolderHeaders } from '../../../utils/folder/folder-headers';
78
import { ASSET_ENDPOINTS } from '../../../utils/constants/endpoints';
89
import { ODATA_PREFIX, ODATA_OFFSET_PARAMS } from '../../../utils/constants/common';
910
import { AssetMap } from '../../../models/orchestrator/assets.constants';
@@ -12,6 +13,7 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '.
1213
import { PaginationHelpers } from '../../../utils/pagination/helpers';
1314
import { PaginationType } from '../../../utils/pagination/internal-types';
1415
import { track } from '../../../core/telemetry';
16+
import { ValidationError } from '../../../core/errors';
1517

1618
/**
1719
* Service for interacting with UiPath Orchestrator Assets API
@@ -155,4 +157,129 @@ export class AssetService extends FolderScopedService implements AssetServiceMod
155157
(raw) => transformData(pascalToCamelCaseKeys(raw), AssetMap),
156158
);
157159
}
160+
161+
/**
162+
* Updates the value of an existing asset by ID.
163+
*
164+
* Fetches the asset internally to determine its type, then updates only the value while
165+
* preserving the asset's name, scope, and description.
166+
*
167+
* **Supported value types:** `Text`, `Integer`, and `Bool` only. Other types
168+
* (`Credential`, `Secret`) throw a `ValidationError`.
169+
*
170+
* The `newValue` runtime type must match the asset's `valueType`:
171+
* - `Text` → `string`
172+
* - `Integer` → `number` (integer)
173+
* - `Bool` → `boolean`
174+
*
175+
* A `ValidationError` is thrown when the `newValue` type does not match.
176+
*
177+
* Returns void; the API does not return the updated asset, so call
178+
* {@link AssetService.getById} or {@link AssetService.getByName} if you need
179+
* the refreshed value.
180+
*
181+
* @param id - Asset ID
182+
* @param newValue - New value to apply (string for `Text`, number for `Integer`, boolean for `Bool`)
183+
* @param options - Folder scoping (`folderId` / `folderKey` / `folderPath`)
184+
* @returns Promise resolving when the asset has been updated
185+
*
186+
* @example
187+
* ```typescript
188+
* import { Assets } from '@uipath/uipath-typescript/assets';
189+
*
190+
* const assets = new Assets(sdk);
191+
*
192+
* // Update a Text asset by folder ID
193+
* await assets.updateValueById(<assetId>, 'new-value', { folderId: <folderId> });
194+
*
195+
* // Update an Integer asset by folder key (GUID)
196+
* await assets.updateValueById(<assetId>, 42, { folderKey: '5f6dadf1-3677-49dc-8aca-c2999dd4b3ba' });
197+
*
198+
* // Update a Bool asset by folder path
199+
* await assets.updateValueById(<assetId>, true, { folderPath: 'Shared/Finance' });
200+
* ```
201+
*/
202+
@track('Assets.UpdateValueById')
203+
async updateValueById(id: number, newValue: AssetNewValue, options?: AssetUpdateValueByIdOptions): Promise<void> {
204+
if (!id) {
205+
throw new ValidationError({ message: 'id is required for updateValueById' });
206+
}
207+
if (newValue === null || newValue === undefined) {
208+
throw new ValidationError({ message: 'newValue is required for updateValueById' });
209+
}
210+
211+
const headers = resolveFolderHeaders({
212+
folderId: options?.folderId,
213+
folderKey: options?.folderKey,
214+
folderPath: options?.folderPath,
215+
resourceType: 'Assets.updateValueById',
216+
fallbackFolderKey: this.config.folderKey,
217+
});
218+
219+
const existingResponse = await this.get<{
220+
Name: string;
221+
ValueScope: AssetValueScope;
222+
ValueType: AssetValueType;
223+
Description: string | null;
224+
}>(
225+
ASSET_ENDPOINTS.GET_BY_ID(id),
226+
{ headers },
227+
);
228+
const existing = existingResponse.data;
229+
230+
const valueField = resolveValueField(id, existing.ValueType, newValue);
231+
232+
const body: Record<string, unknown> = {
233+
Id: id,
234+
Name: existing.Name,
235+
ValueScope: existing.ValueScope,
236+
ValueType: existing.ValueType,
237+
Description: existing.Description,
238+
[valueField]: newValue,
239+
};
240+
241+
await this.put(
242+
ASSET_ENDPOINTS.GET_BY_ID(id),
243+
body,
244+
{ headers },
245+
);
246+
}
247+
}
248+
249+
/**
250+
* Maps the asset's `valueType` to the PUT body field carrying the new value, validating
251+
* that the new value's runtime type matches the asset type.
252+
*/
253+
function resolveValueField(
254+
id: number,
255+
valueType: AssetValueType,
256+
newValue: AssetNewValue,
257+
): 'StringValue' | 'IntValue' | 'BoolValue' {
258+
switch (valueType) {
259+
case AssetValueType.Text:
260+
if (typeof newValue !== 'string') {
261+
throw new ValidationError({
262+
message: `Asset ${id} has valueType Text; newValue must be a string, got ${typeof newValue}`,
263+
});
264+
}
265+
return 'StringValue';
266+
case AssetValueType.Integer:
267+
if (typeof newValue !== 'number' || !Number.isInteger(newValue)) {
268+
throw new ValidationError({
269+
message: `Asset ${id} has valueType Integer; newValue must be an integer number, got ${typeof newValue}`,
270+
});
271+
}
272+
return 'IntValue';
273+
case AssetValueType.Bool:
274+
if (typeof newValue !== 'boolean') {
275+
throw new ValidationError({
276+
message: `Asset ${id} has valueType Bool; newValue must be a boolean, got ${typeof newValue}`,
277+
});
278+
}
279+
return 'BoolValue';
280+
default:
281+
throw new ValidationError({
282+
message: `updateValueById only supports Text, Integer, or Bool assets; asset ${id} has valueType ${valueType}`,
283+
});
284+
}
158285
}

tests/integration/shared/orchestrator/assets.integration.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe.each(modes)('Orchestrator Assets - Integration Tests [%s]', (mode) => {
7777
expect(result.name).toBeDefined();
7878
expect(result.valueType).toBeDefined();
7979
expect(typeof result.name).toBe('string');
80-
});
80+
});
8181
});
8282

8383
describe('getByName', () => {
@@ -159,6 +159,42 @@ describe.each(modes)('Orchestrator Assets - Integration Tests [%s]', (mode) => {
159159
});
160160
});
161161

162+
describe('updateValueById', () => {
163+
it('should update an existing text asset and persist the change', async () => {
164+
const { assets } = getServices();
165+
const config = getTestConfig();
166+
167+
if (!config.folderId) {
168+
throw new Error('INTEGRATION_TEST_FOLDER_ID must be configured to test updateValueById');
169+
}
170+
const folderId = Number(config.folderId);
171+
172+
// Find an existing Text-type asset in the folder so we can update it safely
173+
const allAssets = await assets.getAll({
174+
folderId,
175+
pageSize: 20,
176+
filter: "ValueType eq 'Text'",
177+
});
178+
179+
if (allAssets.items.length === 0) {
180+
throw new Error('No Text-type assets available in the configured folder to exercise updateValueById');
181+
}
182+
183+
const target = allAssets.items[0];
184+
const previousValue = target.value ?? '';
185+
const newValue = `sdk-test-${Date.now()}`;
186+
187+
const result = await assets.updateValueById(target.id, newValue, { folderId });
188+
expect(result).toBeUndefined();
189+
190+
const refreshed = await assets.getById(target.id, folderId);
191+
expect(refreshed.value).toBe(newValue);
192+
193+
// Restore the asset so the suite is idempotent
194+
await assets.updateValueById(target.id, previousValue, { folderId });
195+
});
196+
});
197+
162198
describe('Asset structure validation', () => {
163199
it('should have expected fields in asset objects', async () => {
164200
const { assets } = getServices();

tests/integration/utils/cleanup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export async function cleanupTestTask(
5454
await retryWithBackoff(async () => {
5555
// Attempt to unassign if assigned
5656
try {
57-
await tasks.unassign({ taskIds: [taskId] });
57+
await tasks.unassign([taskId]);
5858
} catch {
5959
// Ignore if already unassigned or can't be unassigned
6060
}

0 commit comments

Comments
 (0)