Skip to content

Commit

Permalink
feat: adding forum thread notification (#337)
Browse files Browse the repository at this point in the history
* feat: adding gsi univid by threadid for table threads for thread notification

* feat: adding lambda for notification

* feat: creating lambda functions and rest api

* fix: fixing Univ Id to univ id
  • Loading branch information
JasonNotJson authored Oct 6, 2023
1 parent 8397927 commit e7137e1
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 0 deletions.
29 changes: 29 additions & 0 deletions lib/constructs/business/rest-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ export class ForumThreadsApiService extends RestApiService {
const threadResource = boardResource.addResource('{thread_id}');
const userResource = root.addResource('user');
const testResource = root.addResource('test');
const notificationResource = root.addResource('notify');

const optionsForumHome = root.addCorsPreflight({
allowOrigins: allowOrigins,
Expand Down Expand Up @@ -779,6 +780,12 @@ export class ForumThreadsApiService extends RestApiService {
],
});

const optionsNotifyThreads = notificationResource.addCorsPreflight({
allowOrigins: allowOrigins,
allowHeaders: allowHeaders,
allowMethods: [apigw2.HttpMethod.GET],
});

const getRespModel = scope.apiEndpoint.addModel('threads-get-resp-model', {
schema: forumThreadGetRespSchema,
contentType: 'application/json',
Expand Down Expand Up @@ -833,6 +840,10 @@ export class ForumThreadsApiService extends RestApiService {
forumThreadsFunctions.deleteFunction,
{ proxy: true },
);
const notifyIntegration = new apigw.LambdaIntegration(
forumThreadsFunctions.getNotificationFunction,
{ proxy: true },
);
const testPostIntegration = new apigw.LambdaIntegration(
forumThreadsFunctions.testPostFunction,
{ proxy: true },
Expand Down Expand Up @@ -940,6 +951,20 @@ export class ForumThreadsApiService extends RestApiService {
requestValidator: props.validator,
},
);
const notifyForumThreads = notificationResource.addMethod(
apigw2.HttpMethod.GET,
notifyIntegration,
{
operationName: 'NotifyThreadCount',
methodResponses: [
{
statusCode: '200',
responseParameters: lambdaRespParams,
},
],
requestValidator: props.validator,
},
);
const testPostForumThreads = testResource.addMethod(
apigw2.HttpMethod.POST,
testPostIntegration,
Expand Down Expand Up @@ -981,6 +1006,10 @@ export class ForumThreadsApiService extends RestApiService {
[apigw2.HttpMethod.GET]: getUserForumThreads,
[apigw2.HttpMethod.OPTIONS]: optionsUserThreads,
},
'/forum/notify': {
[apigw2.HttpMethod.GET]: notifyForumThreads,
[apigw2.HttpMethod.OPTIONS]: optionsNotifyThreads,
},
'/forum/{board_id}': {
[apigw2.HttpMethod.POST]: postForumThreads,
[apigw2.HttpMethod.OPTIONS]: optionsForumBoards,
Expand Down
18 changes: 18 additions & 0 deletions lib/constructs/common/lambda-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ export class ForumThreadFunctions extends Construct {
readonly postFunction: lambda.Function;
readonly patchFunction: lambda.Function;
readonly deleteFunction: lambda.Function;
readonly getNotificationFunction: lambda.Function;
readonly testPostFunction: lambda.Function;
readonly testGetFunction: lambda.Function;

Expand Down Expand Up @@ -645,6 +646,23 @@ export class ForumThreadFunctions extends Construct {
environment: props.envVars,
});

this.getNotificationFunction = new lambda_py.PythonFunction(
this,
'notify-thread',
{
entry: 'src/lambda/get-thread-notify',
description:
'return forum thread count from given date in the database.',
functionName: 'get-thread-notify',
logRetention: logs.RetentionDays.ONE_MONTH,
memorySize: 128,
role: DBReadRole,
runtime: lambda.Runtime.PYTHON_3_9,
timeout: Duration.seconds(3),
environment: props.envVars,
},
);

this.testPostFunction = new lambda_py.PythonFunction(
this,
'test-post-thread',
Expand Down
6 changes: 6 additions & 0 deletions lib/constructs/persistence/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ export class DynamoDatabase extends Construct {
sortKey: { name: 'thread_id', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
this.tables[Collection.THREAD].addGlobalSecondaryIndex({
indexName: 'UnivIDbyThreadIDIndex',
partitionKey: { name: 'univ_id', type: dynamodb.AttributeType.NUMBER },
sortKey: { name: 'thread_id', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});

// this.tables[Collection.THREAD].addLocalSecondaryIndex({
// indexName: 'GroupIndex',
Expand Down
34 changes: 34 additions & 0 deletions src/lambda/get-thread-notify/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from boto3.dynamodb.conditions import Key, Attr
from utils import JsonPayloadBuilder, table, resp_handler


@resp_handler
def get_thread_notify(last_checked_date):

univ_id = 1

lower_bound_thread_id = f"{last_checked_date}_"
# Query the GSI
response = table.query(
IndexName='UnivIDbyThreadIDIndex',
KeyConditionExpression=Key('univ_id').eq(univ_id) & Key(
'thread_id').gt(lower_bound_thread_id),
ScanIndexForward=False # Sorting by thread_id in descending order
)

count = len(response['Items'])

body = JsonPayloadBuilder().add_status(True)\
.add_data(count)\
.add_message('').compile()

return body


def handler(event, context):

params = {
"last_checked_date": event['queryStringParameters'].get('lastChecked', '20230912201031')
}

return get_thread_notify(**params)
82 changes: 82 additions & 0 deletions src/lambda/get-thread-notify/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import boto3
import json
import logging
import os
from decimal import Decimal

db = boto3.resource("dynamodb", region_name="ap-northeast-1")
table = db.Table(os.getenv('TABLE_NAME'))

s3_client = boto3.client('s3')
bucket = os.getenv('BUCKET_NAME')


class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return json.JSONEncoder.default(self, obj)


class JsonPayloadBuilder:
payload = {}

def add_status(self, success):
self.payload['success'] = success
return self

def add_data(self, data):
self.payload['data'] = data
return self

def add_message(self, msg):
self.payload['message'] = msg
return self

def compile(self):
return json.dumps(self.payload, cls=DecimalEncoder, ensure_ascii=False).encode('utf8')


def api_response(code, body):
return {
"isBase64Encoded": False,
"statusCode": code,
'headers': {
"Access-Control-Allow-Origin": '*',
"Content-Type": "application/json",
"Referrer-Policy": "origin"
},
"multiValueHeaders": {"Access-Control-Allow-Methods": ["POST", "OPTIONS", "GET", "PATCH", "DELETE"]},
"body": body
}


def resp_handler(func):
def handle(*args, **kwargs):
try:
resp = func(*args, **kwargs)
return api_response(200, resp)
except LookupError:
resp = JsonPayloadBuilder().add_status(False).add_data(None) \
.add_message("Not found").compile()
return api_response(404, resp)
except Exception as e:
logging.error(str(e))
resp = JsonPayloadBuilder().add_status(False).add_data(None) \
.add_message("Internal error, please contact [email protected].").compile()
return api_response(500, resp)

return handle


def generate_url(bucket_name, object_key, expiration=3600):
try:
response = s3_client.generate_presigned_url('get_object',
Params={'Bucket': bucket_name,
'Key': object_key},
ExpiresIn=expiration)
except Exception as e:
logging.error(str(e))
return None

return response

0 comments on commit e7137e1

Please sign in to comment.