Skip to content

Commit

Permalink
#67 Convert s3 and sqs service into DI classes
Browse files Browse the repository at this point in the history
  • Loading branch information
danielemery committed Nov 2, 2023
1 parent 955b612 commit 6dce99e
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 118 deletions.
40 changes: 40 additions & 0 deletions src/file/s3.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner';
import { createRequest } from '@aws-sdk/util-create-request';
import { formatUrl } from '@aws-sdk/util-format-url';

import config from '../config/config';

export class S3FileService {
#s3Client: S3Client;
constructor() {
this.#s3Client = new S3Client({ region: config.AWS_REGION });
}

async generateSignedUploadUrl(key: string): Promise<string> {
const request = await createRequest(
this.#s3Client,
new PutObjectCommand({
Key: key,
Bucket: config.AWS_BUCKET_NAME,
}),
);

const signer = new S3RequestPresigner({
...this.#s3Client.config,
});

const url = await signer.presign(request, {
expiresIn: 3600,
});
return formatUrl(url);
}

keyToUrl(key: string): string {
return `${config.FILE_ACCESS_BASE_URL}/${key}`;
}

createKey(resourceId: string, fileName: string) {
return `${resourceId}/${fileName}`;
}
}
35 changes: 0 additions & 35 deletions src/file/s3.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import { GraphQLScalarType, Kind } from 'graphql';
import http from 'http';
import * as Sentry from '@sentry/node';

import { authenticationService } from './service.locator';
import { authenticationService, queueService } from './service.locator';
import config from './config/config';
import typeDefs from './gql';
import { persistence } from './persistence/persistence';
import { createQuiz, quiz, quizzes, completeQuiz } from './quiz/quizResolvers';
import { me, users } from './user/userResolvers';
import { subscribeToFileUploads } from './queue/sqs';

const QUIZLORD_VERSION_HEADER = 'X-Quizlord-Api-Version';

Expand Down Expand Up @@ -143,7 +142,7 @@ async function initialise() {
);
app.use(Sentry.Handlers.errorHandler());

subscribeToFileUploads();
queueService.subscribeToFileUploads();
await new Promise<void>((resolve) => httpServer.listen({ port: 4000 }, resolve));

console.log(`🚀 Server ready at http://localhost:4000/`);
Expand Down
81 changes: 81 additions & 0 deletions src/queue/sqs.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { SQSClient, ReceiveMessageCommand, Message, DeleteMessageCommand } from '@aws-sdk/client-sqs';

import config from '../config/config';
import { persistence } from '../persistence/persistence';

interface S3MessageContent {
Records?: S3MessageContentRecord[];
}

interface S3MessageContentRecord {
eventTime: string;
eventName: string;
s3: {
object: {
key: string;
size: number;
};
};
}

export class SQSQueueService {
#client: SQSClient;

constructor() {
this.#client = new SQSClient({ region: config.AWS_REGION });
}

async subscribeToFileUploads() {
// todo exit this loop when app entering shutdown state.
// eslint-disable-next-line no-constant-condition
while (true) {
console.log(`Polling ${config.AWS_FILE_UPLOADED_SQS_QUEUE_URL} for messages`);
const result = await this.#client.send(
new ReceiveMessageCommand({
QueueUrl: config.AWS_FILE_UPLOADED_SQS_QUEUE_URL,
WaitTimeSeconds: 10,
}),
);
if (result.Messages) {
await Promise.all(result.Messages.map((message) => this.processMessage(message)));
}
}
}

async processMessage(message: Message) {
if (message.Body) {
const messageBody = JSON.parse(message.Body);
if (messageBody.Message) {
const messageData: S3MessageContent = JSON.parse(messageBody.Message);
if (messageData.Records) {
await Promise.all(messageData.Records.map((record) => this.processUploadedItem(record)));
}
} else {
console.warn(`Unexpected empty inner message body`, message);
}
} else {
console.warn(`Unexpected empty message body`, message);
}

await this.#client.send(
new DeleteMessageCommand({
QueueUrl: config.AWS_FILE_UPLOADED_SQS_QUEUE_URL,
ReceiptHandle: message.ReceiptHandle,
}),
);
}

async processUploadedItem(record: S3MessageContentRecord) {
console.log('Processing uploaded item');
if (record.eventName !== 'ObjectCreated:Put') {
console.warn(`Unexpected event name <${record.eventName}>`);
}
const key = record.s3.object.key;
const quiz = await persistence.getQuizImage(key);
if (quiz) {
await persistence.markQuizImageReady(key);
} else {
console.error(`Invalid file upload at key: ${key}`);
}
}
}
75 changes: 0 additions & 75 deletions src/queue/sqs.ts

This file was deleted.

9 changes: 4 additions & 5 deletions src/quiz/quizResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import { v4 as uuidv4 } from 'uuid';
import { QuizlordContext } from '..';
import { Quiz, QuizDetails, QuizCompletion, QuizImage, CreateQuizResult, QuizFilters } from '../models';
import { persistence } from '../persistence/persistence';
import { createKey, generateSignedUploadUrl, keyToUrl } from '../file/s3';
import { base64Decode, base64Encode, PagedResult } from '../util/paging-helpers';
import { authorisationService } from '../service.locator';
import { authorisationService, fileService } from '../service.locator';

function quizImagePersistenceToQuizImage(quizImage: QuizImagePersistence): QuizImage {
return {
imageLink: keyToUrl(quizImage.imageKey),
imageLink: fileService.keyToUrl(quizImage.imageKey),
state: quizImage.state,
type: quizImage.type,
};
Expand Down Expand Up @@ -114,7 +113,7 @@ export async function quiz(_: unknown, { id }: { id: string }, context: Quizlord
}

async function populateFileWithUploadLink(file: { fileName: string; type: QuizImageType; imageKey: string }) {
const uploadLink = await generateSignedUploadUrl(file.imageKey);
const uploadLink = await fileService.generateSignedUploadUrl(file.imageKey);
return {
...file,
uploadLink,
Expand All @@ -128,7 +127,7 @@ export async function createQuiz(
): Promise<CreateQuizResult> {
authorisationService.requireUserRole(context, 'USER');
const uuid = uuidv4();
const filesWithKeys = files.map((file) => ({ ...file, imageKey: createKey(uuid, file.fileName) }));
const filesWithKeys = files.map((file) => ({ ...file, imageKey: fileService.createKey(uuid, file.fileName) }));
const [createdQuiz, ...uploadLinks] = await Promise.all([
persistence.createQuizWithImages(
{
Expand Down
8 changes: 8 additions & 0 deletions src/service.locator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { AuthenticationService } from './auth/authentication.service';
import { AuthorisationService } from './auth/authorisation.service';
import { S3FileService } from './file/s3.service';
import { SQSQueueService } from './queue/sqs.service';

// auth
export const authenticationService = new AuthenticationService();
export const authorisationService = new AuthorisationService();

// file
export const fileService = new S3FileService();

// queue
export const queueService = new SQSQueueService();

0 comments on commit 6dce99e

Please sign in to comment.