Skip to content
Open
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
30 changes: 30 additions & 0 deletions app/linkCoffeeToCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Logger } from "@aws-lambda-powertools/logger";
import { AppSyncEvent } from "./interfaces/base-interface";
import { CoffeeManager, ICoffeeManager } from "./services/coffeeManager";

const logger = new Logger({
logLevel: "DEBUG",
serviceName: "LinkCoffeeToCategoryHandler",
});

const coffeeManager: ICoffeeManager = new CoffeeManager();

export async function handler(event: AppSyncEvent) {
logger.info(`🎫 - Received event: ${JSON.stringify(event)}`);

const coffeeId = event.arguments.coffeeId;
const categoryId = event.arguments.categoryId;

if (!coffeeId || !categoryId) {
logger.error('❌ - Missing required parameters: coffeeId or categoryId');
throw new Error('Missing required parameters: coffeeId or categoryId');
}

try {
const wasLink = await coffeeManager.linkCoffeeToCategory(coffeeId, categoryId);
return wasLink;
} catch (error: any) {
logger.error(`❌ - Error linking coffee to category, error: ${error.message}`);
throw new Error(`Error linking coffee with ID ${coffeeId} to category with ID ${categoryId}`);
}
}
2 changes: 1 addition & 1 deletion app/listAllCategorys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CategoryManager, ICategoryManager } from "./services/categoryManager";

const logger = new Logger({
logLevel: "DEBUG",
serviceName: "listAllCategoryCategoryHandler",
serviceName: "listAllCategoryHandler",
});

const categoryManager: ICategoryManager = new CategoryManager();
Expand Down
1,403 changes: 730 additions & 673 deletions app/package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"description": "",
"dependencies": {
"@aws-lambda-powertools/logger": "^2.12.0",
"@aws-sdk/client-dynamodb": "^3.716.0",
"@aws-sdk/lib-dynamodb": "^3.716.0",
"@aws-sdk/client-dynamodb": "^3.675.0",
"@aws-sdk/client-sns": "^3.675.0",
"@aws-sdk/lib-dynamodb": "^3.675.0",
"aws-lambda": "^1.0.7"
}
}
}
39 changes: 38 additions & 1 deletion app/repositories/categoryDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ICategoryDAO {
getCategoryById(id: string): Promise<CategoryModel | null>;
deleteCategoryById(id: string): Promise<boolean>;
updateCategory(category:CategoryModel):Promise<CategoryModel>;
linkCategoryToCoffee(categoryId: string, coffeeId: string): Promise<boolean | null>
}

export class CategoryDAO implements ICategoryDAO {
Expand Down Expand Up @@ -180,7 +181,7 @@ export class CategoryDAO implements ICategoryDAO {
* @returns {Promise<CategoryModel>} - A promise that resolves to the updated CategoryModel.
* @throws {Error} - Throws an error if the update operation fails.
*/
async updateCategory(category:CategoryModel):Promise<CategoryModel>{
async updateCategory(category: CategoryModel):Promise<CategoryModel>{
this.logger.info(`🔄 - Init process to update category in dynamoDB, ${JSON.stringify(category)}`)
category.updatedAt = new Date().toISOString();
const command = new PutCommand({
Expand All @@ -194,10 +195,46 @@ export class CategoryDAO implements ICategoryDAO {
throw new Error(`❌ Error to update a new coffee getted status code ${result.$metadata.httpStatusCode}`);
}
this.logger.info(`✅ - Updated category in dynamoDB with Sucess`);

return category;
} catch (error: any) {
this.logger.error(`❌ - Error to update a new coffee, error: ${error.message}`);
throw new Error(`❌ - Error to update a new coffee, error: ${error.message}`);
}
}

async linkCategoryToCoffee(categoryId: string, coffeeId: string): Promise<boolean | null> {
this.logger.info(`🔄 - Init process to link category (${categoryId}) to coffee (${coffeeId}) in DynamoDB`);

const categoryData = await this.getCategoryById(categoryId);

const commands = new PutCommand({
TableName: process.env.TABLE_NAME,
Item: {
PK: `${Entitys.CATEGORY}#${categoryId}`,
SK: `${Entitys.COFFEE}#${coffeeId}`,
DATA: categoryData ? categoryData.toItem() : null,
},
});

try {
this.logger.info(`🔄 - Sending PutCommands to DynamoDB`);

const [categoryToCoffeeResult] = await Promise.all([
this.ddb.send(commands),
]);

if (categoryToCoffeeResult.$metadata.httpStatusCode !== 200) {
throw new Error(
`Error linking category to coffee. Status codes: CategoryToCoffee - ${categoryToCoffeeResult.$metadata.httpStatusCode}`
);
}

this.logger.info(`✅ - Successfully linked category (${categoryId}) to coffee (${coffeeId}) in DynamoDB`);
return true;
} catch (error: any) {
this.logger.error(`❌ - Error linking category (${categoryId}) to coffee (${coffeeId}), error: ${error.message}`);
throw new Error(`❌ - Error linking category to coffee, error: ${error.message}`);
}
}
}
79 changes: 79 additions & 0 deletions app/repositories/cofffeeDao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { DynamoDBDocumentClient, PutCommand, QueryCommand, DeleteCommand } from
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBCoffeeItem } from "../interfaces/coffee-interfaces";
import { Entitys } from "../enums/base-enum";
import { CategoryModel } from "../model/categoryModel";
import { CategoryDAO } from "./categoryDao";

export interface ICoffeeDAO {
createCoffee(coffee: CoffeeModel): Promise<CoffeeModel>;
updateCoffee(coffee: CoffeeModel, id: string): Promise<CoffeeModel>
deleteCoffee(id: string): Promise<Boolean>;
listAllCoffees(): Promise<CoffeeModel[]>;
getCoffeeById(id: string): Promise<CoffeeModel | null>;
linkCoffeeToCategory(coffeeId: string, categoryId: string): Promise<boolean | null>
}

export class CoffeeDAO implements ICoffeeDAO {
Expand Down Expand Up @@ -75,6 +78,8 @@ export class CoffeeDAO implements ICoffeeDAO {
throw new Error(`Error to updated a coffee getted status code ${result.$metadata.httpStatusCode}`);
}

await this.updateCoffeeLinks(coffee, id);

this.logger.info(`✅ - Updated coffee in dynamoDB with Sucess`);
return coffee;
} catch (error: any) {
Expand Down Expand Up @@ -171,4 +176,78 @@ export class CoffeeDAO implements ICoffeeDAO {
throw new Error(`❌ - Error retrieving coffee by ID, error: ${error.message}`);
}
}

async linkCoffeeToCategory(coffeeId: string, categoryId: string): Promise<boolean | null> {
this.logger.info(`🔄 - Init process to link coffee (${coffeeId}) to category (${categoryId}) in DynamoDB`);

const coffeeData = await this.getCoffeeById(coffeeId);

const command = new PutCommand({
TableName: process.env.TABLE_NAME,
Item: {
PK: `${Entitys.COFFEE}#${coffeeId}`,
SK: `${Entitys.CATEGORY}#${categoryId}`,
DATA: coffeeData ? coffeeData.toItem() : null,
},
});

try {
this.logger.info(`🔄 - Sending PutCommands to DynamoDB`);

const [coffeeToCategoryResult] = await Promise.all([
this.ddb.send(command),
]);

if (coffeeToCategoryResult.$metadata.httpStatusCode !== 200) {
throw new Error(
`Error linking coffee to category. Status codes: CoffeeToCategory - ${coffeeToCategoryResult.$metadata.httpStatusCode}`
);
}

this.logger.info(`✅ - Successfully linked coffee (${coffeeId}) to category (${categoryId}) in DynamoDB`);
return true;
} catch (error: any) {
this.logger.error(`❌ - Error linking coffee (${coffeeId}) to category (${categoryId}), error: ${error.message}`);
throw new Error(`❌ - Error linking coffee to category, error: ${error.message}`);
}
}

async updateCoffeeLinks(coffee: CoffeeModel, coffeeId: string): Promise<void> {
this.logger.info(`🔄 - Init process to update all links for coffee ID: ${coffeeId}`);

const commands = new QueryCommand({
TableName: process.env.TABLE_NAME,
KeyConditionExpression: "PK = :coffeeId",
ExpressionAttributeValues: {
":coffeeId": `${Entitys.COFFEE}#${coffeeId}`,
},
});

try {
const result = await this.ddb.send(commands);

if (result.Items && result.Items.length > 0) {
this.logger.info(`🔄 - Found ${result.Items.length} links to update`);

const updateCommands = result.Items.map((item) => {
return new PutCommand({
TableName: process.env.TABLE_NAME,
Item: {
...item,
DATA: coffee.toItem(),
},
});
});

await Promise.all(updateCommands.map((cmd) => this.ddb.send(cmd)));

this.logger.info(`✅ - Successfully updated all links for coffee ID: ${coffeeId}`);
} else {
this.logger.warn(`⚠️ - No links found to update for coffee ID: ${coffeeId}`);
}
} catch (error: any) {
this.logger.error(`❌ - Error updating links for coffee ID: ${coffeeId}, error: ${error.message}`);
throw new Error(`Error updating links for coffee ID: ${coffeeId}, error: ${error.message}`);
}
}
}
39 changes: 36 additions & 3 deletions app/services/coffeeManager.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
import { Logger } from "@aws-lambda-powertools/logger";
import { CoffeeModel } from "../model/coffeeModel";
import { ICoffeeDAO, CoffeeDAO } from "../repositories/cofffeeDao";
import { ICategoryDAO, CategoryDAO } from "../repositories/categoryDao";
import { ISNSService, SnsCoffeeEventsStatus, SnsCoffeeEventsType, SNSService } from "./snsService";
import { SNSClient } from "@aws-sdk/client-sns";

export interface ICoffeeManager {
createCoffee(coffee: CoffeeModel): Promise<CoffeeModel>;
updateCoffee(coffee: CoffeeModel, id: string): Promise<CoffeeModel>
deleteCoffee(id: string): Promise<Boolean>;
listAllCoffees(): Promise<CoffeeModel[]>;
getCoffeeById(id: string): Promise<CoffeeModel | null>;
linkCoffeeToCategory(coffeeId: string, categoryId: string): Promise<boolean | null>
}

export class CoffeeManager implements ICoffeeManager {

private logger: Logger;
private coffeeDAO: ICoffeeDAO;
private categoryDAO: ICategoryDAO;
private snsService?: ISNSService | undefined;

constructor() {
constructor(snsClient?: SNSClient) {
this.logger = new Logger({
logLevel: "DEBUG",
serviceName: "CoffeeManager",
});
this.coffeeDAO = new CoffeeDAO();
this.categoryDAO = new CategoryDAO();
this.snsService = snsClient ? new SNSService(snsClient) : undefined;
}

async createCoffee(coffee: CoffeeModel): Promise<CoffeeModel> {
Expand All @@ -38,7 +45,12 @@ export class CoffeeManager implements ICoffeeManager {

async updateCoffee(coffee: CoffeeModel, id: string): Promise<CoffeeModel> {
try {
return await this.coffeeDAO.updateCoffee(coffee, id);
const response = await this.coffeeDAO.updateCoffee(coffee, id);
let payload = this.snsService?.buildPayload(response.data, SnsCoffeeEventsType.COFFEE, SnsCoffeeEventsStatus.UPDATE);

await this.snsService?.publishSNS(process.env.SNS_TOPIC_ARN!, payload!);

return response;
} catch (error: any) {
this.logger.error(`❌ - Error to create a coffee, error: ${error.message}`);
throw new Error(`❌ - Error to create a coffee, error: ${error.message}`);
Expand Down Expand Up @@ -66,4 +78,25 @@ export class CoffeeManager implements ICoffeeManager {
throw new Error(`❌ - Error to retrieving coffee, error: ${error.message}`);
}
}

async linkCoffeeToCategory(coffeeId: string, categoryId: string): Promise<boolean | null> {
try {
this.logger.info(`🔄 - Init process to link coffee (${coffeeId}) to category (${categoryId})`);

const coffeeResult = await this.coffeeDAO.linkCoffeeToCategory(coffeeId, categoryId);
const categoryResult = await this.categoryDAO.linkCategoryToCoffee(categoryId, coffeeId);

if (coffeeResult && categoryResult) {
this.logger.info(`✅ - Successfully linked coffee (${coffeeId}) to category (${categoryId})`);
return true;
}

this.logger.warn(`⚠️ - Partial success in linking coffee (${coffeeId}) to category (${categoryId})`);
return null;
} catch (error: any) {
this.logger.error(`❌ - Error linking coffee (${coffeeId}) to category (${categoryId}), error: ${error.message}`);
throw new Error(`❌ - Error linking coffee to category, error: ${error.message}`);
}
}

}
87 changes: 87 additions & 0 deletions app/services/snsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Logger } from "@aws-lambda-powertools/logger";
import { PublishCommand, SNSClient } from "@aws-sdk/client-sns"
import { randomUUID } from "crypto";

export enum SnsCoffeeEventsType {
COFFEE = "COFFEE",
CATEGORY = "CATEGORY"
}

export enum SnsCoffeeEventsStatus {
UPDATE = "UPDATE"
}

export interface CoffeeEventPayload {
eventCreatedAt: string;
eventId:string,
data: any;
eventSource: string;
subject:string,
eventType: SnsCoffeeEventsType;
status: SnsCoffeeEventsStatus;
}

export interface ISNSService {
publishSNS(snsArn: string, payload: CoffeeEventPayload): Promise<void>;
buildPayload(data: any, snsEventType: SnsCoffeeEventsType, snsEventStatus: SnsCoffeeEventsStatus, subject?: string): CoffeeEventPayload;
}

export class SNSService{
private snsClient: SNSClient;
private logger: Logger;

constructor(snsClient: SNSClient) {
this.logger = new Logger({
logLevel: "DEBUG",
serviceName: "SNSClient",
});
this.snsClient = snsClient;
}

async publishSNS(snsArn: string, payload: CoffeeEventPayload): Promise<void> {
this.logger.info(`🔄 - Starting SNS publish process`);

if (!snsArn) {
this.logger.error(`❌ - SNS ARN is missing. Unable to publish the message.`);
throw new Error("SNS ARN is required to publish the message.");
}

if (!payload) {
this.logger.error(`❌ - Payload is missing. Unable to publish the message.`);
throw new Error("Payload is required to publish the message.");
}

this.logger.info(`📤 - Preparing to publish message to SNS ARN: ${snsArn}`);

try {
const command = new PublishCommand({
TopicArn: snsArn,
Message: JSON.stringify(payload),
MessageStructure: "json",
});

const result = await this.snsClient.send(command);

if (result.$metadata.httpStatusCode === 200) {
this.logger.info(`✅ - Message published successfully to SNS ARN: ${snsArn}`);
} else {
this.logger.warn(`⚠️ - Message published to SNS but with unexpected status code: ${result.$metadata.httpStatusCode}`);
}
} catch (error: any) {
this.logger.error(`❌ - Failed to publish message to SNS ARN: ${snsArn}, error: ${error.message}`);
throw new Error(`Error publishing message to SNS: ${error.message}`);
}
}

buildPayload(data: any, snsEventType: SnsCoffeeEventsType, snsEventStatus: SnsCoffeeEventsStatus, subject: string = "COFFEE"): CoffeeEventPayload {
return {
data: JSON.stringify(data),
eventCreatedAt: new Date().toISOString(),
eventId: randomUUID(),
eventSource: "svc-coffee-api",
eventType: snsEventType,
status: snsEventStatus,
subject: subject,
}
}
}
Loading