Skip to content

Commit 3d296da

Browse files
authored
Users by ids (#35)
1 parent 917e9b7 commit 3d296da

File tree

6 files changed

+235
-0
lines changed

6 files changed

+235
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {AppRouteHandler, Response} from '@gravity-ui/expresskit';
2+
3+
import {ApiTag} from '../../components/api-docs';
4+
import {makeReqParser, z, zc} from '../../components/zod';
5+
import {CONTENT_TYPE_JSON} from '../../constants/content-type';
6+
import {getUsersByIds} from '../../services/users/get-users-by-Ids';
7+
8+
import {UsersByIdsResponseModel, usersByIdsModel} from './response-models/users-model';
9+
10+
const requestSchema = {
11+
body: z.object({
12+
subjectIds: zc.decodeIdArray({min: 1, max: 1000}),
13+
}),
14+
};
15+
16+
const parseReq = makeReqParser(requestSchema);
17+
18+
const controller: AppRouteHandler = async (req, res: Response<UsersByIdsResponseModel>) => {
19+
const {body} = await parseReq(req);
20+
21+
const result = await getUsersByIds(
22+
{ctx: req.ctx},
23+
{
24+
subjectIds: body.subjectIds,
25+
},
26+
);
27+
28+
res.status(200).send(await usersByIdsModel.format(result));
29+
};
30+
31+
controller.api = {
32+
summary: 'Users list by ids',
33+
tags: [ApiTag.Users],
34+
request: {
35+
body: {
36+
content: {
37+
[CONTENT_TYPE_JSON]: {
38+
schema: requestSchema.body,
39+
},
40+
},
41+
},
42+
},
43+
responses: {
44+
200: {
45+
description: usersByIdsModel.schema.description ?? '',
46+
content: {
47+
[CONTENT_TYPE_JSON]: {
48+
schema: usersByIdsModel.schema,
49+
},
50+
},
51+
},
52+
},
53+
};
54+
55+
export {controller as getUsersByIds};

src/controllers/users/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {getUserProfile} from './get-user-profile';
2+
import {getUsersByIds} from './get-users-by-ids';
23
import {getUsersList} from './get-users-list';
34
import {updateUserPassword} from './update-user-password';
45
import {updateUserProfile} from './update-user-profile';
56

67
export default {
78
getUsersList,
89
getUserProfile,
10+
getUsersByIds,
911
updateUserPassword,
1012
updateUserProfile,
1113
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {z} from '../../../components/zod';
2+
import type {getUsersByIds} from '../../../services/users/get-users-by-Ids';
3+
import {encodeId, macrotasksMap} from '../../../utils/ids';
4+
5+
const userSchema = z
6+
.strictObject({
7+
userId: z.string(),
8+
login: z.string().nullable(),
9+
email: z.string().nullable(),
10+
firstName: z.string().nullable(),
11+
lastName: z.string().nullable(),
12+
})
13+
.describe('User model');
14+
15+
const schema = z
16+
.strictObject({
17+
users: userSchema.array(),
18+
})
19+
.describe('Users by ids');
20+
21+
export type UsersByIdsResponseModel = z.infer<typeof schema>;
22+
23+
const format = async (
24+
data: Awaited<ReturnType<typeof getUsersByIds>>,
25+
): Promise<z.infer<typeof schema>> => {
26+
return {
27+
users: await macrotasksMap(data.users, (user) => ({
28+
userId: encodeId(user.userId),
29+
login: user.login,
30+
email: user.email,
31+
firstName: user.firstName,
32+
lastName: user.lastName,
33+
})),
34+
};
35+
};
36+
37+
export const usersByIdsModel = {
38+
schema,
39+
format,
40+
};

src/routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ export function getRoutes(_nodekit: NodeKit, options: GetRoutesOptions) {
154154
apiHeaders: [AUTHORIZATION_HEADER],
155155
permission: Permission.InstanceUse,
156156
}),
157+
getUsersByIds: makeRoute({
158+
route: 'POST /v1/users/get-by-ids',
159+
handler: usersController.getUsersByIds,
160+
apiHeaders: [AUTHORIZATION_HEADER],
161+
permission: Permission.InstanceUse,
162+
}),
157163
getMyUserProfile: makeRoute({
158164
route: 'GET /v1/users/me/profile',
159165
handler: usersController.getUserProfile,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {UserModel, UserModelColumn} from '../../db/models/user';
2+
import type {BigIntId} from '../../db/types/id';
3+
import {getReplica} from '../../db/utils/db';
4+
import {ServiceArgs} from '../../types/service';
5+
6+
export interface GetUsersByIdsArgs {
7+
subjectIds: BigIntId[];
8+
}
9+
10+
export const getUsersByIds = async ({ctx, trx}: ServiceArgs, args: GetUsersByIdsArgs) => {
11+
const {subjectIds} = args;
12+
13+
ctx.log('GET_USERS_BY_IDS', {idsSize: subjectIds.length});
14+
15+
const users = await UserModel.query(getReplica(trx))
16+
.select([
17+
UserModelColumn.UserId,
18+
UserModelColumn.Login,
19+
UserModelColumn.Email,
20+
UserModelColumn.FirstName,
21+
UserModelColumn.LastName,
22+
])
23+
.whereIn(UserModelColumn.UserId, Array.from(new Set(subjectIds)))
24+
.limit(1000)
25+
.timeout(UserModel.DEFAULT_QUERY_TIMEOUT);
26+
27+
const userIdToUser: Record<BigIntId, UserModel> = {};
28+
for (const user of users) {
29+
userIdToUser[user.userId] = user;
30+
}
31+
32+
const result = subjectIds.map((subjectId) => userIdToUser[subjectId]).filter(Boolean);
33+
34+
ctx.log('GET_USERS_BY_IDS_SUCCESS', {usersSize: result.length});
35+
36+
return {users: result};
37+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import pick from 'lodash/pick';
2+
import request from 'supertest';
3+
4+
import {UserModel, UserModelColumn} from '../../../../../../db/models/user';
5+
import {encodeId} from '../../../../../../utils/ids';
6+
import {UserRole, app, auth} from '../../../../auth';
7+
import {createTestUsers, generateTokens} from '../../../../helpers';
8+
import {makeRoute} from '../../../../routes';
9+
10+
type CreatedUsers = {
11+
user1: ReturnType<typeof pickCreatedUserFields>;
12+
user2: ReturnType<typeof pickCreatedUserFields>;
13+
user3: ReturnType<typeof pickCreatedUserFields>;
14+
};
15+
16+
const pickCreatedUserFields = (user: UserModel) => ({
17+
...pick(user, [
18+
UserModelColumn.Login,
19+
UserModelColumn.Email,
20+
UserModelColumn.FirstName,
21+
UserModelColumn.LastName,
22+
]),
23+
userId: encodeId(user.userId),
24+
});
25+
26+
describe('Get users by ids', () => {
27+
let userTokens = {} as Awaited<ReturnType<typeof generateTokens>>;
28+
const createdUsers = {} as CreatedUsers;
29+
30+
beforeAll(async () => {
31+
const user1 = await createTestUsers({
32+
login: 'user1',
33+
firstName: 'User1',
34+
35+
roles: [UserRole.Visitor],
36+
});
37+
createdUsers['user1'] = pickCreatedUserFields(user1);
38+
39+
const user2 = await createTestUsers({
40+
login: 'user2',
41+
firstName: 'User2',
42+
});
43+
createdUsers['user2'] = pickCreatedUserFields(user2);
44+
45+
const user3 = await createTestUsers({
46+
login: 'user3',
47+
});
48+
createdUsers['user3'] = pickCreatedUserFields(user3);
49+
50+
userTokens = await generateTokens({userId: user1.userId});
51+
});
52+
53+
test('Access denied without the token', async () => {
54+
const response = await request(app)
55+
.post(makeRoute('getUsersByIds'))
56+
.send({
57+
subjectIds: [createdUsers.user2.userId],
58+
});
59+
expect(response.status).toBe(401);
60+
});
61+
62+
test('User can get all users', async () => {
63+
const response = await auth(request(app).post(makeRoute('getUsersByIds')), {
64+
accessToken: userTokens.accessToken,
65+
}).send({
66+
subjectIds: [
67+
createdUsers.user1.userId,
68+
createdUsers.user2.userId,
69+
createdUsers.user3.userId,
70+
],
71+
});
72+
73+
expect(response.status).toBe(200);
74+
expect(response.body).toStrictEqual({
75+
users: expect.toIncludeSameMembers([
76+
createdUsers['user1'],
77+
createdUsers['user2'],
78+
createdUsers['user3'],
79+
]),
80+
});
81+
});
82+
83+
test('Skip not exists id', async () => {
84+
const response = await auth(request(app).post(makeRoute('getUsersByIds')), {
85+
accessToken: userTokens.accessToken,
86+
}).send({
87+
subjectIds: [createdUsers.user1.userId, 'jghw6klpj6tm5'],
88+
});
89+
90+
expect(response.status).toBe(200);
91+
expect(response.body).toStrictEqual({
92+
users: expect.toIncludeSameMembers([createdUsers['user1']]),
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)