Skip to content

Commit

Permalink
Merge pull request #7 from cieslarmichal/feature/list-users
Browse files Browse the repository at this point in the history
add get users endpoint
  • Loading branch information
cieslarmichal authored Feb 9, 2024
2 parents 55db7fb + 18a6141 commit 32d396c
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import {
type FindUserPathParamsDTO,
type FindUserResponseBodyDTO,
} from './schemas/findUserSchema.js';
import {
type FindUsersQueryParamsDTO,
findUsersQueryParamsDTOSchema,
findUsersResponseBodyDTOSchema,
type FindUsersResponseBodyDTO,
} from './schemas/findUsersSchema.js';
import {
type GrantBucketAccessResponseBodyDTO,
type GrantBucketAccessBodyDTO,
Expand Down Expand Up @@ -50,6 +56,7 @@ import { type DeleteUserCommandHandler } from '../../../application/commandHandl
import { type GrantBucketAccessCommandHandler } from '../../../application/commandHandlers/grantBucketAccessCommandHandler/grantBucketAccessCommandHandler.js';
import { type RevokeBucketAccessCommandHandler } from '../../../application/commandHandlers/revokeBucketAccessCommandHandler/revokeBucketAccessCommandHandler.js';
import { type FindUserQueryHandler } from '../../../application/queryHandlers/findUserQueryHandler/findUserQueryHandler.js';
import { type FindUsersQueryHandler } from '../../../application/queryHandlers/findUsersQueryHandler/findUsersQueryHandler.js';
import { type User } from '../../../domain/entities/user/user.js';
import { type UserDTO } from '../common/userDTO.js';

Expand All @@ -60,6 +67,7 @@ export class AdminUserHttpController implements HttpController {
private readonly createUserCommandHandler: CreateUserCommandHandler,
private readonly deleteUserCommandHandler: DeleteUserCommandHandler,
private readonly findUserQueryHandler: FindUserQueryHandler,
private readonly findUsersQueryHandler: FindUsersQueryHandler,
private readonly grantBucketAccessCommandHandler: GrantBucketAccessCommandHandler,
private readonly revokeBucketAccessCommandHandler: RevokeBucketAccessCommandHandler,
private readonly accessControlService: AccessControlService,
Expand Down Expand Up @@ -144,6 +152,24 @@ export class AdminUserHttpController implements HttpController {
tags: ['User'],
description: 'Find user by id.',
}),
new HttpRoute({
method: HttpMethodName.get,
handler: this.findUsers.bind(this),
schema: {
request: {
queryParams: findUsersQueryParamsDTOSchema,
},
response: {
[HttpStatusCode.ok]: {
schema: findUsersResponseBodyDTOSchema,
description: 'Users found.',
},
},
},
securityMode: SecurityMode.bearer,
tags: ['User'],
description: 'Find users.',
}),
new HttpRoute({
method: HttpMethodName.delete,
path: ':id',
Expand Down Expand Up @@ -251,6 +277,36 @@ export class AdminUserHttpController implements HttpController {
};
}

private async findUsers(
request: HttpRequest<undefined, FindUsersQueryParamsDTO, undefined>,
): Promise<HttpOkResponse<FindUsersResponseBodyDTO>> {
await this.accessControlService.verifyBearerToken({
authorizationHeader: request.headers['authorization'],
expectedRole: UserRole.admin,
});

const page = request.queryParams.page ?? 1;

const pageSize = request.queryParams.pageSize ?? 10;

const { users, totalUsers } = await this.findUsersQueryHandler.execute({
page,
pageSize,
});

return {
statusCode: HttpStatusCode.ok,
body: {
data: users.map((user) => this.mapUserToUserDTO(user)),
metadata: {
page,
pageSize,
totalPages: Math.ceil(totalUsers / pageSize),
},
},
};
}

private async deleteUser(
request: HttpRequest<undefined, undefined, DeleteUserPathParamsDTO>,
): Promise<HttpNoContentResponse<DeleteUserResponseBodyDTO>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type Static, Type } from '@sinclair/typebox';

import type * as contracts from '@common/contracts';

import { type TypeExtends } from '../../../../../../common/types/schemaExtends.js';
import { userDTOSchema } from '../../common/userDTO.js';

export const findUsersQueryParamsDTOSchema = Type.Object({
page: Type.Optional(Type.Integer({ minimum: 1 })),
pageSize: Type.Optional(Type.Integer({ minimum: 1 })),
});

export type FindUsersQueryParamsDTO = TypeExtends<
Static<typeof findUsersQueryParamsDTOSchema>,
contracts.FindUsersQueryParams
>;

export const findUsersResponseBodyDTOSchema = Type.Object({
data: Type.Array(userDTOSchema),
metadata: Type.Object({
page: Type.Integer(),
pageSize: Type.Integer(),
totalPages: Type.Integer(),
}),
});

export type FindUsersResponseBodyDTO = TypeExtends<
Static<typeof findUsersResponseBodyDTOSchema>,
contracts.FindUsersResponseBody
>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type QueryHandler } from '../../../../../common/types/queryHandler.js';
import { type User } from '../../../domain/entities/user/user.js';

export interface FindUsersQueryHandlerPayload {
readonly page: number;
readonly pageSize: number;
}

export interface FindUsersQueryHandlerResult {
readonly users: User[];
readonly totalUsers: number;
}

export type FindUsersQueryHandler = QueryHandler<FindUsersQueryHandlerPayload, FindUsersQueryHandlerResult>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { beforeEach, afterEach, expect, it, describe } from 'vitest';

import { type FindUsersQueryHandler } from './findUsersQueryHandler.js';
import { Application } from '../../../../../core/application.js';
import { type SqliteDatabaseClient } from '../../../../../core/database/sqliteDatabaseClient/sqliteDatabaseClient.js';
import { coreSymbols } from '../../../../../core/symbols.js';
import { symbols } from '../../../symbols.js';
import { UserTestUtils } from '../../../tests/utils/userTestUtils/userTestUtils.js';

describe('FindUsersQueryHandler', () => {
let findUsersQueryHandler: FindUsersQueryHandler;

let sqliteDatabaseClient: SqliteDatabaseClient;

let userTestUtils: UserTestUtils;

beforeEach(async () => {
const container = Application.createContainer();

findUsersQueryHandler = container.get<FindUsersQueryHandler>(symbols.findUsersQueryHandler);

sqliteDatabaseClient = container.get<SqliteDatabaseClient>(coreSymbols.sqliteDatabaseClient);

userTestUtils = new UserTestUtils(sqliteDatabaseClient);

await userTestUtils.truncate();
});

afterEach(async () => {
await userTestUtils.truncate();

await sqliteDatabaseClient.destroy();
});

it('finds Users', async () => {
const user1 = await userTestUtils.createAndPersist();

const user2 = await userTestUtils.createAndPersist();

const result = await findUsersQueryHandler.execute({
page: 1,
pageSize: 10,
});

expect(result.users[0]?.getId()).toEqual(user1.id);

expect(result.users[1]?.getId()).toEqual(user2.id);

expect(result.totalUsers).toBe(2);
});

it('paginates Users', async () => {
const user1 = await userTestUtils.createAndPersist();

await userTestUtils.createAndPersist();

const result = await findUsersQueryHandler.execute({
page: 1,
pageSize: 1,
});

expect(result.users[0]?.getId()).toEqual(user1.id);

expect(result.totalUsers).toBe(2);
});

it('returns empty array if no Users found', async () => {
const result = await findUsersQueryHandler.execute({
page: 1,
pageSize: 10,
});

expect(result.users).toEqual([]);

expect(result.totalUsers).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
type FindUsersQueryHandler,
type FindUsersQueryHandlerPayload,
type FindUsersQueryHandlerResult,
} from './findUsersQueryHandler.js';
import { type UserRepository } from '../../../domain/repositories/userRepository/userRepository.js';

export class FindUsersQueryHandlerImpl implements FindUsersQueryHandler {
public constructor(private readonly userRepository: UserRepository) {}

public async execute(payload: FindUsersQueryHandlerPayload): Promise<FindUsersQueryHandlerResult> {
const { page, pageSize } = payload;

const [users, totalUsers] = await Promise.all([
this.userRepository.findUsers({
page,
pageSize,
}),
this.userRepository.countUsers(),
]);

return {
users,
totalUsers,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ export interface FindUserPayload {
readonly email?: string;
}

export interface FindUsersPayload {
readonly page: number;
readonly pageSize: number;
}

export interface FindUsersResult {
readonly users: User[];
}

export interface FindUserTokensPayload {
readonly userId: string;
}
Expand All @@ -34,6 +43,8 @@ export interface DeleteUserPayload {
export interface UserRepository {
createUser(input: CreateUserPayload): Promise<User>;
findUser(input: FindUserPayload): Promise<User | null>;
findUsers(input: FindUsersPayload): Promise<User[]>;
countUsers(): Promise<number>;
findUserTokens(input: FindUserTokensPayload): Promise<UserTokens | null>;
findUserBuckets(input: FindUserBucketsPayload): Promise<UserBucket[]>;
updateUser(input: UpdateUserPayload): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,63 @@ describe('UserRepositoryImpl', () => {
});
});

describe('Find all', () => {
it('finds all Users', async () => {
const user1 = await userTestUtils.createAndPersist();

const user2 = await userTestUtils.createAndPersist();

const users = await userRepository.findUsers({
page: 1,
pageSize: 10,
});

expect(users.length).toEqual(2);

expect(users.find((user) => user.getId() === user1.id)).not.toBeNull();

expect(users.find((user) => user.getId() === user2.id)).not.toBeNull();
});

it('returns empty array if there are no Users', async () => {
const users = await userRepository.findUsers({
page: 1,
pageSize: 10,
});

expect(users.length).toEqual(0);
});

it('returns empty array if there are no Users on the given page', async () => {
await userTestUtils.createAndPersist();

const users = await userRepository.findUsers({
page: 2,
pageSize: 10,
});

expect(users.length).toEqual(0);
});
});

describe('Count', () => {
it('counts Users', async () => {
await userTestUtils.createAndPersist();

await userTestUtils.createAndPersist();

const count = await userRepository.countUsers();

expect(count).toEqual(2);
});

it('returns 0 if there are no Users', async () => {
const count = await userRepository.countUsers();

expect(count).toEqual(0);
});
});

describe('Update', () => {
it(`creates User's refresh tokens`, async () => {
const user = await userTestUtils.createAndPersist();
Expand Down
Loading

0 comments on commit 32d396c

Please sign in to comment.