Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/api-client/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
Segment,
Team,
User,
UpdateUserData,
Metric,
Application,
Environment,
Expand Down Expand Up @@ -755,7 +756,7 @@ export class APIClient {
return this.validateEntityResponse<User>(response, 'user', 'createUser');
}

async updateUser(id: UserId, data: Partial<User>): Promise<User> {
async updateUser(id: UserId, data: UpdateUserData): Promise<User> {
const response = await this.request('PUT', `/users/${id}`, { data: { data } });
return this.validateEntityResponse<User>(response, 'user', 'updateUser');
}
Expand Down
3 changes: 3 additions & 0 deletions src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Segment as OpenAPISegment,
Team as OpenAPITeam,
User as OpenAPIUser,
UpdateUserData,
Metric as OpenAPIMetric,
MetricTag as OpenAPIMetricTag,
MetricCategory as OpenAPIMetricCategory,
Expand Down Expand Up @@ -217,6 +218,8 @@ export type User = Partial<OpenAPIUser> & {
email: string;
};

export type { UpdateUserData };

export type Metric = Partial<OpenAPIMetric> & {
id: MetricId;
name: string;
Expand Down
8 changes: 8 additions & 0 deletions src/commands/users/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ describe('users command', () => {
});
});

it('should update a user role via --role', async () => {
await usersCommand.parseAsync(['node', 'test', 'update', '1', '--role', '34']);

expect(mockClient.updateUser).toHaveBeenCalledWith(1, {
roles: [{ role_id: 34 }],
});
});

it('should archive a user', async () => {
await usersCommand.parseAsync(['node', 'test', 'archive', '1']);

Expand Down
15 changes: 14 additions & 1 deletion src/core/users/update.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { APIClient } from '../../api-client/api-client.js';
import type { UserId } from '../../lib/api/branded-types.js';
import type { UpdateUserData } from '../../api-client/types.js';
import type { CommandResult } from '../types.js';

export interface UpdateUserParams {
Expand All @@ -12,13 +13,25 @@ export async function updateUser(
client: APIClient,
params: UpdateUserParams
): Promise<CommandResult<void>> {
const data: Record<string, string> = {};
const data: UpdateUserData = {};
if (params.name) {
const parts = params.name.split(' ');
data.first_name = parts[0] ?? '';
data.last_name = parts.slice(1).join(' ');
}

if (params.role !== undefined) {
// The API expects roles as an array of { role_id } objects for the
// global team (see UpdateUserBody in the OpenAPI schema and the backend
// handler which does data.roles.map(({ role_id }) => ...)). A bare
// number array is rejected. --role carries a single role ID.
const roleId = Number(params.role);
if (!Number.isInteger(roleId) || roleId <= 0) {
throw new Error(`Invalid role: "${params.role}" is not a valid role ID`);
}
data.roles = [{ role_id: roleId }];
}

if (Object.keys(data).length === 0) {
throw new Error('At least one update field is required');
}
Expand Down
30 changes: 30 additions & 0 deletions src/core/users/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,34 @@ describe('updateUser', () => {
'At least one update field is required'
);
});

it('should update role alone as [{ role_id }]', async () => {
mockClient.updateUser.mockResolvedValue(undefined);

const result = await updateUser(mockClient, { id: 5 as any, role: '34' });

expect(mockClient.updateUser).toHaveBeenCalledWith(5, {
roles: [{ role_id: 34 }],
});
expect(result).toEqual({ data: undefined });
});

it('should update name and role together', async () => {
mockClient.updateUser.mockResolvedValue(undefined);

await updateUser(mockClient, { id: 7 as any, name: 'Jane Smith', role: '2' });

expect(mockClient.updateUser).toHaveBeenCalledWith(7, {
first_name: 'Jane',
last_name: 'Smith',
roles: [{ role_id: 2 }],
});
});

it('should throw on an invalid role value', async () => {
await expect(updateUser(mockClient, { id: 5 as any, role: 'admin' })).rejects.toThrow(
'Invalid role'
);
expect(mockClient.updateUser).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions src/lib/api/openapi-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type GoalTag = components['schemas']['GoalTag'];
export type Segment = components['schemas']['Segment'];
export type Team = components['schemas']['Team'];
export type User = components['schemas']['User'];
export type UpdateUserData = components['schemas']['UpdateUserBody']['data'];
export type Metric = components['schemas']['Metric'];
export type MetricTag = components['schemas']['MetricTag'];
export type MetricCategory = components['schemas']['MetricCategory'];
Expand Down