Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add get users endpoint #7

Merged
merged 1 commit into from
Feb 9, 2024
Merged
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
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
Loading