Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/oauth-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This page lists the specific OAuth scopes required in external app for each SDK
| `getAll()` | `OR.Assets` or `OR.Assets.Read` |
| `getById()` | `OR.Assets` or `OR.Assets.Read` |
| `getByName()` | `OR.Assets` or `OR.Assets.Read` |
| `updateValueById()` | `OR.Assets` or `OR.Assets.Write` |

## Jobs

Expand Down
35 changes: 34 additions & 1 deletion src/models/orchestrator/assets.models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AssetGetAllOptions, AssetGetResponse, AssetGetByIdOptions, AssetGetByNameOptions } from './assets.types';
import { AssetGetAllOptions, AssetGetResponse, AssetGetByIdOptions, AssetGetByNameOptions, AssetNewValue, AssetUpdateValueByIdOptions } from './assets.types';
import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination';

/**
Expand Down Expand Up @@ -90,4 +90,37 @@ export interface AssetServiceModel {
* ```
*/
getByName(name: string, options?: AssetGetByNameOptions): Promise<AssetGetResponse>;

/**
* Updates the value of an existing asset by ID.
*
* Fetches the asset internally to determine its type, then updates only the value while
* preserving the asset's name, scope, and description.
*
* **Supported value types:** `Text`, `Integer`, and `Bool` only. Other types
* (`Credential`, `Secret`) throw a `ValidationError`.
Comment thread
maninder-uipath marked this conversation as resolved.
Comment thread
maninder-uipath marked this conversation as resolved.
*
* The `newValue` runtime type must match the asset's `valueType`:
* - `Text` → `string`
* - `Integer` → `number` (integer)
* - `Bool` → `boolean`
*
* @param id - Asset ID
* @param newValue - New value to apply (string for `Text`, number for `Integer`, boolean for `Bool`)
* @param options - Folder scoping (`folderId` / `folderKey` / `folderPath`)
* @returns Promise resolving when the asset has been updated
*
* @example
* ```typescript
* // Update a Text asset by folder ID
* await assets.updateValueById(<assetId>, 'new-value', { folderId: <folderId> });
*
* // Update an Integer asset by folder key (GUID)
* await assets.updateValueById(<assetId>, 42, { folderKey: '5f6dadf1-3677-49dc-8aca-c2999dd4b3ba' });
*
* // Update a Bool asset by folder path
* await assets.updateValueById(<assetId>, true, { folderPath: 'Shared/Finance' });
* ```
*/
updateValueById(id: number, newValue: AssetNewValue, options?: AssetUpdateValueByIdOptions): Promise<void>;
}
19 changes: 15 additions & 4 deletions src/models/orchestrator/assets.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@ export enum AssetValueScope {
* Enum for Asset Value Type
*/
export enum AssetValueType {
Comment thread
maninder-uipath marked this conversation as resolved.
DBConnectionString = 'DBConnectionString',
HttpConnectionString = 'HttpConnectionString',
Text = 'Text',
Bool = 'Bool',
Integer = 'Integer',
Credential = 'Credential',
WindowsCredential = 'WindowsCredential',
KeyValueList = 'KeyValueList',
Secret = 'Secret'
}

Expand Down Expand Up @@ -73,3 +69,18 @@ export interface AssetGetByIdOptions extends BaseOptions {}
* Options for getting a single asset by name
*/
export interface AssetGetByNameOptions extends FolderScopedOptions {}

/**
* New value accepted by {@link AssetServiceModel.updateValueById}.
*
* The runtime type must match the asset's `valueType`:
* - `Text` → `string`
* - `Integer` → `number`
* - `Bool` → `boolean`
*/
export type AssetNewValue = string | number | boolean;
Comment thread
maninder-uipath marked this conversation as resolved.

/**
* Options for updating an asset value by ID
*/
export interface AssetUpdateValueByIdOptions extends FolderScopedOptions {}
123 changes: 122 additions & 1 deletion src/services/orchestrator/assets/assets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FolderScopedService } from '../../folder-scoped';
import { AssetGetResponse, AssetGetAllOptions, AssetGetByIdOptions, AssetGetByNameOptions } from '../../../models/orchestrator/assets.types';
import { AssetGetResponse, AssetGetAllOptions, AssetGetByIdOptions, AssetGetByNameOptions, AssetNewValue, AssetUpdateValueByIdOptions, AssetValueScope, AssetValueType } from '../../../models/orchestrator/assets.types';
import { AssetServiceModel } from '../../../models/orchestrator/assets.models';
import { addPrefixToKeys, pascalToCamelCaseKeys, transformData } from '../../../utils/transform';
import { createHeaders } from '../../../utils/http/headers';
import { FOLDER_ID } from '../../../utils/constants/headers';
import { resolveFolderHeaders } from '../../../utils/folder/folder-headers';
import { ASSET_ENDPOINTS } from '../../../utils/constants/endpoints';
import { ODATA_PREFIX, ODATA_OFFSET_PARAMS } from '../../../utils/constants/common';
import { AssetMap } from '../../../models/orchestrator/assets.constants';
Expand All @@ -12,6 +13,7 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '.
import { PaginationHelpers } from '../../../utils/pagination/helpers';
import { PaginationType } from '../../../utils/pagination/internal-types';
import { track } from '../../../core/telemetry';
import { ValidationError } from '../../../core/errors';

/**
* Service for interacting with UiPath Orchestrator Assets API
Expand Down Expand Up @@ -155,4 +157,123 @@ export class AssetService extends FolderScopedService implements AssetServiceMod
(raw) => transformData(pascalToCamelCaseKeys(raw), AssetMap),
);
}

/**
* Updates the value of an existing asset by ID.
*
* Fetches the asset internally to determine its type, then updates only the value while
* preserving the asset's name, scope, and description.
*
* **Supported value types:** `Text`, `Integer`, and `Bool` only. Other types
* (`Credential`, `Secret`) throw a `ValidationError`.
*
* The `newValue` runtime type must match the asset's `valueType`:
* - `Text` → `string`
* - `Integer` → `number` (integer)
* - `Bool` → `boolean`
*
* @param id - Asset ID
* @param newValue - New value to apply (string for `Text`, number for `Integer`, boolean for `Bool`)
* @param options - Folder scoping (`folderId` / `folderKey` / `folderPath`)
* @returns Promise resolving when the asset has been updated
*
* @example
* ```typescript
* import { Assets } from '@uipath/uipath-typescript/assets';
*
* const assets = new Assets(sdk);
*
* // Update a Text asset by folder ID
* await assets.updateValueById(<assetId>, 'new-value', { folderId: <folderId> });
*
* // Update an Integer asset by folder key (GUID)
* await assets.updateValueById(<assetId>, 42, { folderKey: '5f6dadf1-3677-49dc-8aca-c2999dd4b3ba' });
*
* // Update a Bool asset by folder path
* await assets.updateValueById(<assetId>, true, { folderPath: 'Shared/Finance' });
* ```
*/
@track('Assets.UpdateValueById')
async updateValueById(id: number, newValue: AssetNewValue, options?: AssetUpdateValueByIdOptions): Promise<void> {
if (!id) {
throw new ValidationError({ message: 'id is required for updateValueById' });
}
if (newValue === null || newValue === undefined) {
throw new ValidationError({ message: 'newValue is required for updateValueById' });
}

const headers = resolveFolderHeaders({
folderId: options?.folderId,
folderKey: options?.folderKey,
folderPath: options?.folderPath,
resourceType: 'Assets.updateValueById',
fallbackFolderKey: this.config.folderKey,
});

const existingResponse = await this.get<{
Name: string;
ValueScope: AssetValueScope;
ValueType: AssetValueType;
Description: string | null;
}>(
ASSET_ENDPOINTS.GET_BY_ID(id),
{ headers },
);
const existing = existingResponse.data;

const valueField = resolveValueField(id, existing.ValueType, newValue);

const body: Record<string, unknown> = {
Id: id,
Name: existing.Name,
ValueScope: existing.ValueScope,
ValueType: existing.ValueType,
Description: existing.Description,
[valueField]: newValue,
};

await this.put(
ASSET_ENDPOINTS.GET_BY_ID(id),
body,
{ headers },
);
}
}

/**
* Maps the asset's `valueType` to the PUT body field carrying the new value, validating
* that the new value's runtime type matches the asset type.
*/
function resolveValueField(
id: number,
valueType: AssetValueType,
newValue: AssetNewValue,
): 'StringValue' | 'IntValue' | 'BoolValue' {
switch (valueType) {
case AssetValueType.Text:
if (typeof newValue !== 'string') {
throw new ValidationError({
message: `Asset ${id} has valueType Text; newValue must be a string, got ${typeof newValue}`,
});
}
return 'StringValue';
case AssetValueType.Integer:
if (typeof newValue !== 'number' || !Number.isInteger(newValue)) {
throw new ValidationError({
message: `Asset ${id} has valueType Integer; newValue must be an integer number, got ${typeof newValue}`,
});
}
return 'IntValue';
case AssetValueType.Bool:
if (typeof newValue !== 'boolean') {
throw new ValidationError({
message: `Asset ${id} has valueType Bool; newValue must be a boolean, got ${typeof newValue}`,
});
}
return 'BoolValue';
default:
throw new ValidationError({
message: `updateValueById only supports Text, Integer, or Bool assets; asset ${id} has valueType ${valueType}`,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,4 @@ describe.each(modes)('Action Center Tasks - Integration Tests [%s]', (mode) => {
await cleanupTestTask(createdTaskId);
}
});
});
}, 120000);
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe.each(modes)('Orchestrator Assets - Integration Tests [%s]', (mode) => {
expect(result.name).toBeDefined();
expect(result.valueType).toBeDefined();
expect(typeof result.name).toBe('string');
});
});
});

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

describe('updateValueById', () => {
it('should update an existing text asset and persist the change', async () => {
const { assets } = getServices();
const config = getTestConfig();

if (!config.folderId) {
throw new Error('INTEGRATION_TEST_FOLDER_ID must be configured to test updateValueById');
}
const folderId = Number(config.folderId);

// Find an existing Text-type asset in the folder so we can update it safely
const allAssets = await assets.getAll({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OData filter values should use the camelCase field names shown in the SDK response, not PascalCase API field names. Per the conventions: "filter, select, expand, orderby values use the field case shown in the SDK response (camelCase for services that transform, else as-is) — not the raw API. Applies within JSDoc examples, tests, and internal calls."

Suggested change
const allAssets = await assets.getAll({
filter: "valueType eq 'Text'",

folderId,
pageSize: 20,
filter: "ValueType eq 'Text'",
});

if (allAssets.items.length === 0) {
throw new Error('No Text-type assets available in the configured folder to exercise updateValueById');
}

const target = allAssets.items[0];
const previousValue = target.value ?? '';
const newValue = `sdk-test-${Date.now()}`;

const result = await assets.updateValueById(target.id, newValue, { folderId });
expect(result).toBeUndefined();

const refreshed = await assets.getById(target.id, folderId);
expect(refreshed.value).toBe(newValue);

// Restore the asset so the suite is idempotent
await assets.updateValueById(target.id, previousValue, { folderId });
});
});

describe('Asset structure validation', () => {
it('should have expected fields in asset objects', async () => {
const { assets } = getServices();
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/utils/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function cleanupTestTask(
await retryWithBackoff(async () => {
// Attempt to unassign if assigned
try {
await tasks.unassign({ taskIds: [taskId] });
await tasks.unassign([taskId]);
} catch {
// Ignore if already unassigned or can't be unassigned
}
Expand Down
Loading
Loading