Merge pull request #1597 from automatisch/rest-api-get-users
feat: Implement api/v1/users API endpoint
This commit is contained in:
@@ -3,7 +3,7 @@ import request from 'supertest';
|
|||||||
import app from '../../../../app.js';
|
import app from '../../../../app.js';
|
||||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
import { createUser } from '../../../../../test/factories/user';
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
import currentUserPayload from '../../../../../test/payloads/current-user.js';
|
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
|
||||||
|
|
||||||
describe('GET /api/v1/users/me', () => {
|
describe('GET /api/v1/users/me', () => {
|
||||||
let role, currentUser, token;
|
let role, currentUser, token;
|
||||||
@@ -20,7 +20,7 @@ describe('GET /api/v1/users/me', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = currentUserPayload(currentUser, role);
|
const expectedPayload = getCurrentUserMock(currentUser, role);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,7 +4,7 @@ import app from '../../../../app.js';
|
|||||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
import { createUser } from '../../../../../test/factories/user';
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
import { createPermission } from '../../../../../test/factories/permission';
|
import { createPermission } from '../../../../../test/factories/permission';
|
||||||
import userPayload from '../../../../../test/payloads/user.js';
|
import getUserMock from '../../../../../test/mocks/rest/api/v1/users/get-user';
|
||||||
|
|
||||||
describe('GET /api/v1/users/:userId', () => {
|
describe('GET /api/v1/users/:userId', () => {
|
||||||
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
||||||
@@ -30,7 +30,7 @@ describe('GET /api/v1/users/:userId', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = userPayload(anotherUser, anotherUserRole);
|
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
18
packages/backend/src/controllers/api/v1/users/get-users.js
Normal file
18
packages/backend/src/controllers/api/v1/users/get-users.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
import User from '../../../../models/user.js';
|
||||||
|
import paginateRest from '../../../../helpers/pagination-rest.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const usersQuery = User.query()
|
||||||
|
.leftJoinRelated({
|
||||||
|
role: true,
|
||||||
|
})
|
||||||
|
.withGraphFetched({
|
||||||
|
role: true,
|
||||||
|
})
|
||||||
|
.orderBy('full_name', 'asc');
|
||||||
|
|
||||||
|
const users = await paginateRest(usersQuery, request.query.page);
|
||||||
|
|
||||||
|
renderObject(response, users);
|
||||||
|
};
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../../../app';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createRole } from '../../../../../test/factories/role';
|
||||||
|
import { createPermission } from '../../../../../test/factories/permission';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getUsersMock from '../../../../../test/mocks/rest/api/v1/users/get-users';
|
||||||
|
|
||||||
|
describe('GET /api/v1/users', () => {
|
||||||
|
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUserRole = await createRole({
|
||||||
|
key: 'currentUser',
|
||||||
|
name: 'Current user role',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'User',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUser = await createUser({
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
fullName: 'Current User',
|
||||||
|
});
|
||||||
|
|
||||||
|
anotherUserRole = await createRole({
|
||||||
|
key: 'anotherUser',
|
||||||
|
name: 'Another user role',
|
||||||
|
});
|
||||||
|
|
||||||
|
anotherUser = await createUser({
|
||||||
|
roleId: anotherUserRole.id,
|
||||||
|
fullName: 'Another User',
|
||||||
|
});
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return users data', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/users')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedResponsePayload = await getUsersMock(
|
||||||
|
[anotherUser, currentUser],
|
||||||
|
[anotherUserRole, currentUserRole]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedResponsePayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -3,6 +3,10 @@ const authorizationList = {
|
|||||||
action: 'read',
|
action: 'read',
|
||||||
subject: 'User',
|
subject: 'User',
|
||||||
},
|
},
|
||||||
|
'/api/v1/users/': {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'User',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authorizeUser = async (request, response, next) => {
|
export const authorizeUser = async (request, response, next) => {
|
||||||
|
25
packages/backend/src/helpers/pagination-rest.js
Normal file
25
packages/backend/src/helpers/pagination-rest.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const paginateRest = async (query, page) => {
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
page = parseInt(page, 10);
|
||||||
|
|
||||||
|
if (isNaN(page) || page < 1) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [records, count] = await Promise.all([
|
||||||
|
query.limit(pageSize).offset((page - 1) * pageSize),
|
||||||
|
query.resultSize(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageInfo: {
|
||||||
|
currentPage: page,
|
||||||
|
totalPages: Math.ceil(count / pageSize),
|
||||||
|
},
|
||||||
|
totalCount: count,
|
||||||
|
records,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default paginateRest;
|
@@ -1,14 +1,27 @@
|
|||||||
|
const isPaginated = (object) =>
|
||||||
|
object?.pageInfo &&
|
||||||
|
object?.totalCount !== undefined &&
|
||||||
|
Array.isArray(object?.records);
|
||||||
|
|
||||||
|
const isArray = (object) =>
|
||||||
|
Array.isArray(object) || Array.isArray(object?.records);
|
||||||
|
|
||||||
|
const totalCount = (object) =>
|
||||||
|
isPaginated(object) ? object.totalCount : isArray(object) ? object.length : 1;
|
||||||
|
|
||||||
const renderObject = (response, object) => {
|
const renderObject = (response, object) => {
|
||||||
const isArray = Array.isArray(object);
|
const data = isPaginated(object) ? object.records : object;
|
||||||
|
|
||||||
const computedPayload = {
|
const computedPayload = {
|
||||||
data: object,
|
data,
|
||||||
meta: {
|
meta: {
|
||||||
type: object.constructor.name,
|
type: isPaginated(object)
|
||||||
count: isArray ? object.length : 1,
|
? object.records[0].constructor.name
|
||||||
isArray,
|
: object.constructor.name,
|
||||||
currentPage: null,
|
count: totalCount(object),
|
||||||
totalPages: null,
|
isArray: isArray(object),
|
||||||
|
currentPage: isPaginated(object) ? object.pageInfo.currentPage : null,
|
||||||
|
totalPages: isPaginated(object) ? object.pageInfo.totalPages : null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -3,9 +3,11 @@ import { authenticateUser } from '../../../helpers/authentication.js';
|
|||||||
import { authorizeUser } from '../../../helpers/authorization.js';
|
import { authorizeUser } from '../../../helpers/authorization.js';
|
||||||
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
|
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
|
||||||
import getUserAction from '../../../controllers/api/v1/users/get-user.js';
|
import getUserAction from '../../../controllers/api/v1/users/get-user.js';
|
||||||
|
import getUsersAction from '../../../controllers/api/v1/users/get-users.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', authenticateUser, authorizeUser, getUsersAction);
|
||||||
router.get('/me', authenticateUser, getCurrentUserAction);
|
router.get('/me', authenticateUser, getCurrentUserAction);
|
||||||
router.get('/:userId', authenticateUser, authorizeUser, getUserAction);
|
router.get('/:userId', authenticateUser, authorizeUser, getUserAction);
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
const currentUserPayload = (currentUser, role) => {
|
const getCurrentUserMock = (currentUser, role) => {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
createdAt: currentUser.createdAt.toISOString(),
|
createdAt: currentUser.createdAt.toISOString(),
|
||||||
@@ -29,4 +29,4 @@ const currentUserPayload = (currentUser, role) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default currentUserPayload;
|
export default getCurrentUserMock;
|
@@ -1,4 +1,4 @@
|
|||||||
const userPayload = (currentUser, role) => {
|
const getUserMock = (currentUser, role) => {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
createdAt: currentUser.createdAt.toISOString(),
|
createdAt: currentUser.createdAt.toISOString(),
|
||||||
@@ -28,4 +28,4 @@ const userPayload = (currentUser, role) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default userPayload;
|
export default getUserMock;
|
38
packages/backend/test/mocks/rest/api/v1/users/get-users.js
Normal file
38
packages/backend/test/mocks/rest/api/v1/users/get-users.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const getUsersMock = async (users, roles) => {
|
||||||
|
const data = users.map((user) => {
|
||||||
|
const role = roles.find((r) => r.id === user.roleId);
|
||||||
|
return {
|
||||||
|
createdAt: user.createdAt.toISOString(),
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.fullName,
|
||||||
|
id: user.id,
|
||||||
|
role: role
|
||||||
|
? {
|
||||||
|
createdAt: role.createdAt.toISOString(),
|
||||||
|
description: role.description,
|
||||||
|
id: role.id,
|
||||||
|
isAdmin: role.isAdmin,
|
||||||
|
key: role.key,
|
||||||
|
name: role.name,
|
||||||
|
updatedAt: role.updatedAt.toISOString(),
|
||||||
|
}
|
||||||
|
: null, // Fallback to null if role not found
|
||||||
|
roleId: user.roleId,
|
||||||
|
trialExpiryDate: user.trialExpiryDate.toISOString(),
|
||||||
|
updatedAt: user.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
meta: {
|
||||||
|
count: data.length,
|
||||||
|
currentPage: 1,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: 1,
|
||||||
|
type: 'User',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getUsersMock;
|
Reference in New Issue
Block a user