Skip to content
Draft
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
7 changes: 6 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
"hooks": [
{
"type": "command",
"if": "Bash(gh pr create:*)",
"command": "jq -r '.tool_input.command' | grep -q 'gh pr create' && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"STOP: You must run /sdk-review before creating a PR. If sdk-review has NOT been run in this session, abort this PR creation and run /sdk-review first. Only proceed with gh pr create after the review passes.\"}}'",
"if": "Bash(gh pr create:*)",
"statusMessage": "Checking pre-PR review requirement..."
}
]
}
]
},
"permissions": {
"allow": [
"Bash(git -c core.editor=true rebase --continue)"
]
}
}
4 changes: 2 additions & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
echo "traces_test_trace_id=${{ secrets.UIPATH_TRACES_TEST_TRACE_ID_DEV || secrets.UIPATH_TRACES_TEST_TRACE_ID }}" >> $GITHUB_OUTPUT
echo "tasks_test_user_group_id=${{ secrets.UIPATH_TASKS_TEST_USER_GROUP_ID_DEV || secrets.UIPATH_TASKS_TEST_USER_GROUP_ID }}" >> $GITHUB_OUTPUT
echo "tasks_test_user_id=${{ secrets.UIPATH_TASKS_TEST_USER_ID_DEV || secrets.UIPATH_TASKS_TEST_USER_ID }}" >> $GITHUB_OUTPUT
echo "identity_test_user_id=${{ secrets.UIPATH_IDENTITY_TEST_USER_ID_DEV || secrets.UIPATH_IDENTITY_TEST_USER_ID }}" >> $GITHUB_OUTPUT
echo "organization_id=${{ secrets.UIPATH_ORGANIZATION_ID_DEV || secrets.UIPATH_ORGANIZATION_ID }}" >> $GITHUB_OUTPUT

- name: Create Integration Test Configuration
run: |
Expand Down Expand Up @@ -106,7 +106,7 @@ jobs:
TRACES_TEST_TRACE_ID=${{ steps.config.outputs.traces_test_trace_id }}
TASKS_TEST_USER_GROUP_ID=${{ steps.config.outputs.tasks_test_user_group_id }}
TASKS_TEST_USER_ID=${{ steps.config.outputs.tasks_test_user_id }}
IDENTITY_TEST_USER_ID=${{ steps.config.outputs.identity_test_user_id }}
UIPATH_ORGANIZATION_ID=${{ steps.config.outputs.organization_id }}
EOF

- name: Run Integration Tests
Expand Down
3 changes: 3 additions & 0 deletions docs/oauth-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,6 @@ User management is authorized by the caller's **organization role** (organizatio
| Method | OAuth Scope |
|--------|-------------|
| `getById()` | None — requires organization administrator role |
| `updateById()` | None — requires organization administrator role |
| `deleteById()` | None — requires organization administrator role |
| `create()` | None — requires organization administrator role |
5 changes: 4 additions & 1 deletion src/models/identity/users.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { UserType, UserCategory } from './users.types';
* User field mappings (API field name → SDK field name).
*
* Semantic renames only — the API already returns camelCase, so no case
* conversion is involved.
* conversion is involved. `groupIDsToAdd`/`groupIDsToRemove` appear only in
* update requests; `transformRequest()` reverses the map for outbound payloads.
*/
export const UserMap: { [key: string]: string } = {
creationTime: 'createdTime',
lastModificationTime: 'lastModifiedTime',
groupIDs: 'groupIds',
groupIDsToAdd: 'groupIdsToAdd',
groupIDsToRemove: 'groupIdsToRemove',
};

/**
Expand Down
13 changes: 13 additions & 0 deletions src/models/identity/users.internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* NOT exported from the public barrel (`src/models/identity/index.ts`).
*/

import type { UserOperationResult } from './users.types';

/**
* Raw user shape as returned by the Identity user management API.
*
Expand Down Expand Up @@ -46,3 +48,14 @@ export interface RawUserEntry {
export const INTERNAL_USER_FIELDS = [
'legacyId',
] as const satisfies ReadonlyArray<keyof RawUserEntry>;

/**
* Raw response of `POST /api/User/BulkCreate`.
*
* The Swagger spec declares a bare `IdentityResult`, but the live API also
* returns the created users.
*/
export interface RawUserCreateResponse {
result: UserOperationResult;
users: RawUserEntry[];
}
169 changes: 163 additions & 6 deletions src/models/identity/users.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,37 @@
* interface that drives generated API documentation.
*/

import type { RawUserGetResponse } from './users.types';
import type {
RawUserGetResponse,
UserCreateData,
UserCreateOptions,
UserOperationResult,
UserUpdateOptions,
UserUpdateResponse,
} from './users.types';

/**
* User returned by the Users service.
* User with attached methods
*/
export interface UserGetResponse extends RawUserGetResponse {}
export type UserGetResponse = RawUserGetResponse & UserMethods;

/**
* Response from `create()`.
*
* `result` reflects the request as a whole; `users` contains the created users.
*/
export interface UserCreateResponse {

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.

Per convention, response types must use type, not interface:

Always use type for response types (intersections, unions, composed types). The only place interface extends is required is single-type aliases (type X = Y), which break TypeDoc.

UserCreateResponse is a plain multi-field object type — it's not a single-type alias extending another type, so the TypeDoc exception doesn't apply.

Suggested change
export interface UserCreateResponse {
export type UserCreateResponse = {

and close with }; on line 30.

/** Overall outcome of the create request. */
result: UserOperationResult;
/** The created users. */
users: UserGetResponse[];
}

/**
* Service model for managing users in a UiPath organization.
*
* Provides organization-level user administration.
* Provides organization-level user administration: retrieving, updating and
* deleting users, creating users in bulk, and inviting users by email.
*
* ### Usage
*
Expand All @@ -31,16 +51,153 @@ export interface UserServiceModel {
* Gets a user by ID.
*
* Returns the full user details including profile fields, group membership,
* activity flags and invitation state.
* activity flags and invitation state, with entity methods attached
* (`update`, `delete`).
*
* @param userId - User GUID
* @returns Promise resolving to a {@link UserGetResponse} with the user's profile fields, group membership, activity flags and invitation state
* @returns Promise resolving to a {@link UserGetResponse} with the user's profile fields, group membership, activity flags and invitation state, plus bound entity methods
*
* @example
* ```typescript
* const user = await users.getById('<userId>');
* console.log(`${user.displayName} (${user.email}) — active: ${user.isActive}`);
*
* // Operate on the user directly via bound methods
* await user.update({ displayName: 'New Name' });
* ```
*/
getById(userId: string): Promise<UserGetResponse>;

/**
* Updates a user. Only the provided fields are changed.
*
* @param userId - User GUID
* @param options - Fields to update
* @returns Promise resolving to a {@link UserUpdateResponse} indicating whether the update succeeded and any errors
*
* @example
* ```typescript
* // First, get the user with users.getById() or from users.create()
* const result = await users.updateById('<userId>', { displayName: 'New Name' });
* if (result.succeeded) {
* console.log('User updated');
* }
* ```
*
* @example Manage group membership
* ```typescript
* await users.updateById('<userId>', {
* groupIdsToAdd: ['<groupId-1>'],
* groupIdsToRemove: ['<groupId-2>'],
* });
* ```
*/
updateById(userId: string, options: UserUpdateOptions): Promise<UserUpdateResponse>;

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.

Per convention, {Entity}{Operation}Options bags — whose fields are all optional — must also have the parameter itself marked ?:

{Entity}{Operation}Options — bag of optional fields. Always the last parameter. Always marked ?. Contains only optional fields.

Same applies to UserMethods.update(options: UserUpdateOptions) at line 163, and the implementation signature in users.ts.

Suggested change
updateById(userId: string, options: UserUpdateOptions): Promise<UserUpdateResponse>;
updateById(userId: string, options?: UserUpdateOptions): Promise<UserUpdateResponse>;


/**
* Deletes a user.
*
* @param userId - User GUID
* @returns Promise that resolves when the user is deleted
*
* @example
* ```typescript
* await users.deleteById('<userId>');
* ```
*/
deleteById(userId: string): Promise<void>;

/**
* Creates users in bulk.
*
* Returns the created users with entity methods attached (`update`, `delete`).
* A single invalid user fails the whole request — check `result.errors` for
* the reason.
*
* @param users - Users to create
* @param organizationId - Organization GUID the users belong to
* @param options - Optional group assignment applied to every created user
* @returns Promise resolving to a {@link UserCreateResponse} with the overall outcome and the created users
*
* @example
* ```typescript
* const response = await users.create(
* [{ userName: 'jdoe', email: 'jdoe@acme.com', name: 'Jane', surname: 'Doe' }],
* '<organizationId>'
* );
* if (response.result.succeeded) {
* console.log(`Created ${response.users[0].id}`);
* }
* ```
*
* @example Add every created user to groups
* ```typescript
* const response = await users.create(
* [{ userName: 'jdoe', email: 'jdoe@acme.com' }],
* '<organizationId>',
* { groupIds: ['<groupId>'] }
* );
* ```
*/
create(
users: UserCreateData[],
organizationId: string,
options?: UserCreateOptions
): Promise<UserCreateResponse>;
}

/**
* Methods attached to user objects returned by `getById()` and `create()`.
*/
export interface UserMethods {
/**
* Updates this user. Only the provided fields are changed.
*
* @param options - Fields to update
* @returns Promise resolving to the operation outcome
*/
update(options: UserUpdateOptions): Promise<UserUpdateResponse>;

/**
* Deletes this user.
*
* @returns Promise that resolves when the user is deleted
*/
delete(): Promise<void>;
}

/**
* Creates methods for a user
*
* @param userData - The user data (response from API)
* @param service - The user service instance
* @returns Object containing user methods
*/
function createUserMethods(userData: RawUserGetResponse, service: UserServiceModel): UserMethods {
return {
async update(options: UserUpdateOptions): Promise<UserUpdateResponse> {
if (!userData.id) throw new Error('User ID is undefined');
return service.updateById(userData.id, options);
},

async delete(): Promise<void> {
if (!userData.id) throw new Error('User ID is undefined');
return service.deleteById(userData.id);
},
};
}

/**
* Attaches methods to a user object
*
* @param userData - The user data (response from API)
* @param service - The user service instance
* @returns User data with methods attached
*/
export function createUserWithMethods(
userData: RawUserGetResponse,
service: UserServiceModel
): UserGetResponse {
const methods = createUserMethods(userData, service);
return Object.assign({}, userData, methods);
}
82 changes: 81 additions & 1 deletion src/models/identity/users.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Identity user management types — response shapes and enums.
* Identity user management types — response shapes, request payloads and enums.
*/

/**
Expand Down Expand Up @@ -72,3 +72,83 @@ export interface RawUserGetResponse {
/** Whether the user has accepted their invitation. */
invitationAccepted: boolean;
}

/**
* Error entry from a failed user operation.

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.

Convention: "Always use type for response types." UserOperationError and UserOperationResult (line 87) both describe API response shapes and should be type, not interface.

(Note: UserUpdateResponse extends UserOperationResult {} is the correct TypeDoc single-alias exception — that pattern works whether UserOperationResult is an interface or a type.)

Suggested change
* Error entry from a failed user operation.
export type UserOperationError = {

*/
export interface UserOperationError {
/** Machine-readable error code (e.g. `InvalidUserName`). */
code: string;
/** Human-readable error description. */
description: string;
}

/**
* Overall outcome of a user operation.
*/
export interface UserOperationResult {
/** Whether the operation succeeded. */
succeeded: boolean;
/** Errors that caused the operation to fail. Empty on success. */
errors: UserOperationError[];
}

/**
* Response from `updateById()`.
*/
export interface UserUpdateResponse extends UserOperationResult {}

/**
* Payload for `updateById()`. Only the provided fields are changed.
*/
export interface UserUpdateOptions {
/** New first name. */
name?: string;
/** New last name. */
surname?: string;
/** New display name. */
displayName?: string;
/** New email address. */
email?: string;
/** Activate (`true`) or deactivate (`false`) the user. */
isActive?: boolean;
/** New password. */
password?: string;
/** GUIDs of groups to add the user to. */
groupIdsToAdd?: string[];
/** GUIDs of groups to remove the user from. */
groupIdsToRemove?: string[];
/** Whether this user bypasses the basic authentication restriction. */
bypassBasicAuthRestriction?: boolean;
/** Whether the user should be marked as having accepted their invitation. */
invitationAccepted?: boolean;
}

/**
* A user to create with `create()`.
*/
export interface UserCreateData {
/** Username — can only contain letters or digits. */
userName: string;
/** Email address. */
email?: string;
/** First name. */
name?: string;
/** Last name. */
surname?: string;
/** Display name. */
displayName?: string;
/** Whether the user should be marked as having accepted their invitation. */
invitationAccepted?: boolean;
/** Whether this user bypasses the basic authentication restriction. */
bypassBasicAuthRestriction?: boolean;
}

/**
* Options for `create()`.
*/
export interface UserCreateOptions {
/** GUIDs of groups every created user is added to. */
groupIds?: string[];
}

2 changes: 1 addition & 1 deletion src/services/identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Identity Users Service Module
*
* Provides organization-level user administration via the UiPath Identity API:
* - `Users` — retrieve users in the organization
* - `Users` — retrieve, update and delete users, and create users in bulk
*
* @example
* ```typescript
Expand Down
Loading
Loading