Skip to content

Commit

Permalink
add creating bucket endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
cieslarmichal committed Feb 14, 2024
1 parent 5d14639 commit 509c130
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
import { UserRole } from '@common/contracts';

import {
type CreateBucketBodyDTO,
type CreateBucketResponseBodyDTO,
createBucketBodyDTOSchema,
createBucketResponseBodyDTOSchema,
} from './schemas/createBucketSchema.js';
import { findBucketsResponseBodyDTOSchema, type FindBucketsResponseBodyDTO } from './schemas/findBucketsSchema.js';
import { type HttpController } from '../../../../../common/types/http/httpController.js';
import { HttpMethodName } from '../../../../../common/types/http/httpMethodName.js';
import { type HttpRequest } from '../../../../../common/types/http/httpRequest.js';
import { type HttpOkResponse } from '../../../../../common/types/http/httpResponse.js';
import { type HttpNoContentResponse, type HttpOkResponse } from '../../../../../common/types/http/httpResponse.js';
import { HttpRoute } from '../../../../../common/types/http/httpRoute.js';
import { HttpStatusCode } from '../../../../../common/types/http/httpStatusCode.js';
import { SecurityMode } from '../../../../../common/types/http/securityMode.js';
import { type AccessControlService } from '../../../../authModule/application/services/accessControlService/accessControlService.js';
import { type CreateBucketCommandHandler } from '../../../application/commandHandlers/createBucketCommandHandler/createBucketCommandHandler.js';
import { type FindBucketsQueryHandler } from '../../../application/queryHandlers/findBucketsQueryHandler/findBucketsQueryHandler.js';

export class AdminResourceHttpController implements HttpController {
public readonly basePath = '/admin/api/buckets';

public constructor(
private readonly findBucketsQueryHandler: FindBucketsQueryHandler,
private readonly createBucketCommandHandler: CreateBucketCommandHandler,
private readonly accessControlService: AccessControlService,
) {}

public getHttpRoutes(): HttpRoute[] {
return [
new HttpRoute({
method: HttpMethodName.post,
handler: this.createBucket.bind(this),
schema: {
request: {
body: createBucketBodyDTOSchema,
},
response: {
[HttpStatusCode.noContent]: {
schema: createBucketResponseBodyDTOSchema,
description: 'Bucket created.',
},
},
},
securityMode: SecurityMode.bearer,
tags: ['Bucket'],
description: 'Create bucket.',
}),
new HttpRoute({
method: HttpMethodName.get,
handler: this.findBuckets.bind(this),
Expand All @@ -40,6 +66,24 @@ export class AdminResourceHttpController implements HttpController {
];
}

private async createBucket(
request: HttpRequest<CreateBucketBodyDTO, undefined, undefined>,
): Promise<HttpNoContentResponse<CreateBucketResponseBodyDTO>> {
await this.accessControlService.verifyBearerToken({
authorizationHeader: request.headers['authorization'],
expectedRole: UserRole.admin,
});

await this.createBucketCommandHandler.execute({
bucketName: request.body.bucketName,
});

return {
statusCode: HttpStatusCode.noContent,
body: null,
};
}

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

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

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

export const createBucketBodyDTOSchema = Type.Object({
bucketName: Type.String({ minLength: 1 }),
});

export type CreateBucketBodyDTO = TypeExtends<Static<typeof createBucketBodyDTOSchema>, contracts.CreateBucketBody>;

export const createBucketResponseBodyDTOSchema = Type.Null();

export type CreateBucketResponseBodyDTO = Static<typeof createBucketResponseBodyDTOSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type CommandHandler } from '../../../../../common/types/commandHandler.js';

export interface CreateBucketCommandHandlerPayload {
readonly bucketName: string;
}

export type CreateBucketCommandHandler = CommandHandler<CreateBucketCommandHandlerPayload, void>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { beforeEach, afterEach, expect, it, describe } from 'vitest';

import { type CreateBucketCommandHandler } from './createBucketCommandHandler.js';
import { testSymbols } from '../../../../../../tests/container/symbols.js';
import { TestContainer } from '../../../../../../tests/container/testContainer.js';
import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js';
import { symbols } from '../../../symbols.js';
import { type S3TestUtils } from '../../../tests/utils/s3TestUtils.js';

describe('CreateBucketCommandHandler', () => {
let commandHandler: CreateBucketCommandHandler;

let s3TestUtils: S3TestUtils;

const bucketName1 = 'resources1';

const bucketName2 = 'resources2';

beforeEach(async () => {
const container = TestContainer.create();

commandHandler = container.get<CreateBucketCommandHandler>(symbols.createBucketCommandHandler);

s3TestUtils = container.get<S3TestUtils>(testSymbols.s3TestUtils);
});

afterEach(async () => {
await Promise.all([s3TestUtils.deleteBucket(bucketName1), s3TestUtils.deleteBucket(bucketName2)]);
});

it('create a bucket', async () => {
await commandHandler.execute({ bucketName: bucketName1 });

const createdBuckets = await s3TestUtils.getBuckets();

expect(createdBuckets.includes(bucketName1)).toBe(true);
});

it('throws an error - when a bucket with the same name already exists', async () => {
await s3TestUtils.createBucket(bucketName2);

try {
await commandHandler.execute({ bucketName: bucketName2 });
} catch (error) {
expect(error instanceof OperationNotValidError);

return;
}

expect.fail();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { CreateBucketCommand, ListBucketsCommand } from '@aws-sdk/client-s3';

import {
type CreateBucketCommandHandler,
type CreateBucketCommandHandlerPayload,
} from './createBucketCommandHandler.js';
import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js';
import { type LoggerService } from '../../../../../libs/logger/services/loggerService/loggerService.js';
import { type S3Client } from '../../../../../libs/s3/clients/s3Client/s3Client.js';

export class CreateBucketCommandHandlerImpl implements CreateBucketCommandHandler {
public constructor(
private readonly s3Client: S3Client,
private readonly loggerService: LoggerService,
) {}

public async execute(payload: CreateBucketCommandHandlerPayload): Promise<void> {
const { bucketName } = payload;

const result = await this.s3Client.send(new ListBucketsCommand({}));

if (result.Buckets?.find((bucket) => bucket.Name === bucketName)) {
throw new OperationNotValidError({
reason: 'Bucket already exists.',
bucketName,
});
}

this.loggerService.debug({
message: 'Creating Bucket...',
bucketName,
});

await this.s3Client.send(new CreateBucketCommand({ Bucket: bucketName }));

this.loggerService.debug({
message: 'Bucket created.',
bucketName,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { TestContainer } from '../../../../../../tests/container/testContainer.j
import { symbols } from '../../../symbols.js';
import { type S3TestUtils } from '../../../tests/utils/s3TestUtils.js';

describe('FindUserBucketsQueryHandler', () => {
describe('FindBucketsQueryHandler', () => {
let queryHandler: FindBucketsQueryHandler;

let s3TestUtils: S3TestUtils;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export class FindBucketsQueryHandlerImpl implements FindBucketsQueryHandler {
public constructor(private readonly s3Client: S3Client) {}

public async execute(): Promise<FindBucketsQueryHandlerResult> {
const command = new ListBucketsCommand({});

const result = await this.s3Client.send(command);
const result = await this.s3Client.send(new ListBucketsCommand({}));

if (!result.Buckets) {
return {
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/modules/resourceModule/resourceModule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AdminResourceHttpController } from './api/httpControllers/adminResourceHttpController/adminResourceHttpController.js';
import { ResourceHttpController } from './api/httpControllers/resourceHttpController/resourceHttpController.js';
import { type CreateBucketCommandHandler } from './application/commandHandlers/createBucketCommandHandler/createBucketCommandHandler.js';
import { CreateBucketCommandHandlerImpl } from './application/commandHandlers/createBucketCommandHandler/createBucketCommandHandlerImpl.js';
import { type DeleteResourceCommandHandler } from './application/commandHandlers/deleteResourceCommandHandler/deleteResourceCommandHandler.js';
import { DeleteResourceCommandHandlerImpl } from './application/commandHandlers/deleteResourceCommandHandler/deleteResourceCommandHandlerImpl.js';
import { type DownloadImageQueryHandler } from './application/queryHandlers/downloadImageQueryHandler/downloadImageQueryHandler.js';
Expand Down Expand Up @@ -87,6 +89,15 @@ export class ResourceModule implements DependencyInjectionModule {
() => new FindBucketsQueryHandlerImpl(container.get<S3Client>(coreSymbols.s3Client)),
);

container.bind<CreateBucketCommandHandler>(
symbols.createBucketCommandHandler,
() =>
new CreateBucketCommandHandlerImpl(
container.get<S3Client>(coreSymbols.s3Client),
container.get<LoggerService>(coreSymbols.loggerService),
),
);

container.bind<ResourceHttpController>(
symbols.resourceHttpController,
() =>
Expand All @@ -106,6 +117,7 @@ export class ResourceModule implements DependencyInjectionModule {
() =>
new AdminResourceHttpController(
container.get<FindBucketsQueryHandler>(symbols.findBucketsQueryHandler),
container.get<CreateBucketCommandHandler>(symbols.createBucketCommandHandler),
container.get<AccessControlService>(authSymbols.accessControlService),
),
);
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/modules/resourceModule/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const symbols = {
deleteResourceCommandHandler: Symbol('deleteResourceCommandHandler'),

findBucketsQueryHandler: Symbol('findBucketsQueryHandler'),
createBucketCommandHandler: Symbol('createBucketCommandHandler'),

resourceHttpController: Symbol('resourceHttpController'),
adminResourceHttpController: Symbol('adminResourceHttpController'),
Expand Down
19 changes: 19 additions & 0 deletions apps/backend/src/modules/resourceModule/tests/utils/s3TestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ListObjectsV2Command,
type ListObjectsV2CommandInput,
PutObjectCommand,
ListBucketsCommand,
} from '@aws-sdk/client-s3';
import { existsSync, readFileSync } from 'node:fs';

Expand Down Expand Up @@ -55,6 +56,12 @@ export class S3TestUtils {
}

public async deleteBucket(bucketName: string): Promise<void> {
const existingBuckets = await this.getBuckets();

if (!existingBuckets.includes(bucketName)) {
return;
}

await this.truncateBucket(bucketName);

const command = new DeleteBucketCommand({
Expand Down Expand Up @@ -109,4 +116,16 @@ export class S3TestUtils {

await this.s3Client.send(command);
}

public async getBuckets(): Promise<string[]> {
const result = await this.s3Client.send(new ListBucketsCommand({}));

if (!result.Buckets) {
return [];
}

const buckets = result.Buckets.map((bucket) => bucket.Name as string);

return buckets;
}
}
2 changes: 2 additions & 0 deletions common/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export * from './schemas/resource/exportResources.js';
export * from './schemas/resource/findUserBuckets.js';

export * from './schemas/resource/findBuckets.js';

export * from './schemas/resource/createBucket.js';
3 changes: 3 additions & 0 deletions common/contracts/src/schemas/resource/createBucket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface CreateBucketBody {
readonly bucketName: string;
}

0 comments on commit 509c130

Please sign in to comment.