From 19d0e0fba8025afe7984c6bd6b90cf2c6ea05aee Mon Sep 17 00:00:00 2001 From: Facundo De Lorenzo Date: Mon, 20 May 2024 11:39:25 -0300 Subject: [PATCH] split products & orders endpoints --- README.md | 4 + packages/api/package.json | 3 +- packages/api/src/orders.ts | 194 ++++++++++++++++++ packages/api/src/products.ts | 66 +----- packages/api/src/types.ts | 29 ++- packages/api/template.yaml | 32 ++- .../components/app/hooks/useCreateOrder.ts | 5 +- packages/front/src/pages/Products.tsx | 2 +- pnpm-lock.yaml | 3 + 9 files changed, 266 insertions(+), 72 deletions(-) create mode 100644 packages/api/src/orders.ts diff --git a/README.md b/README.md index a505477..9478651 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,12 @@ Tests for the backend were not implemented. If they were to be implemented, I'd use jest. I'd write a test for every lambda handler, so as to check that they can handle recieving wrong parameters, and also to ensure that they get their job done as it's meant to be, which is important to keep the business rules working rightfully. +[Outdated] For the db i was on my way to implement dynamo but run out of time (Friday 6pm) I'll try to make it work for monday :) +[new] +Dynamo db added to store orders, +Api allows to list and finish orders but UI wasn't developed. diff --git a/packages/api/package.json b/packages/api/package.json index 2fb9f34..a13922a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.577.0", - "@aws-sdk/lib-dynamodb": "^3.577.0" + "@aws-sdk/lib-dynamodb": "^3.577.0", + "uuid": "^9.0.1" } } diff --git a/packages/api/src/orders.ts b/packages/api/src/orders.ts new file mode 100644 index 0000000..1957900 --- /dev/null +++ b/packages/api/src/orders.ts @@ -0,0 +1,194 @@ +import { APIGatewayEvent, Callback, Context } from 'aws-lambda'; +import { CreateOrderRequest, FinishOrderRequest, Order, OrderDTO } from './types'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { + DynamoDBDocumentClient, + PutCommand, + PutCommandOutput, + QueryCommand, + QueryCommandOutput, + UpdateCommand, + UpdateCommandOutput, +} from '@aws-sdk/lib-dynamodb'; +import { v4 as uuid } from 'uuid'; + +const client = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(client); + +const responseHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Credentials': true, + 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', +}; + +exports.get = async ( + event: APIGatewayEvent, + context: Context, + callback: Callback, +) => { + try { + const req = event.queryStringParameters; + const restaurantId = parseInt(req.restaurantId); + if (!restaurantId) + return { + isBase64Encoded: false, + statusCode: 400, + headers: responseHeaders, + body: 'restaurantId must be provided', + }; + + const command = new QueryCommand({ + TableName: 'wakeup-challenge-orders', + ExpressionAttributeValues: { + ':id': restaurantId, + ':status': 'active', + }, + KeyConditionExpression: 'restaurantId = :id', + FilterExpression: 'orderStatus = :status', + }); + const response = await docClient.send(command) as Omit & { Items: Order[] }; + + const orders: OrderDTO[] = response.Items.map((x) => ({ + id: x.orderId, + totalPrice: x.totalPrice, + products: x.products, + })); + + return { + isBase64Encoded: false, + statusCode: 200, + headers: responseHeaders, + body: JSON.stringify({ orders }), + }; + } catch (error) { + return { + isBase64Encoded: false, + statusCode: 500, + headers: responseHeaders, + body: error.message, + }; + } +}; + +exports.create = async ( + event: APIGatewayEvent, + context: Context, + callback: Callback, +) => { + try { + const req: CreateOrderRequest = JSON.parse(event.body); + + if (!req?.products) + return { + isBase64Encoded: false, + statusCode: 400, + headers: responseHeaders, + body: 'products must be provided', + }; + if (!req?.restaurantId) + return { + isBase64Encoded: false, + statusCode: 400, + headers: responseHeaders, + body: 'restaurantId must be provided', + }; + + const orderId = uuid(); + const totalPrice = req.products.reduce( + (acc, val) => acc + val.product.price, + 0, + ); + const command = new PutCommand({ + TableName: 'wakeup-challenge-orders', + Item: { + restaurantId: req.restaurantId, + orderId, + products: req.products, + totalPrice, + orderStatus: 'active', + }, + }); + await docClient.send(command) as Omit & { Item: Order }; + + const order: OrderDTO = { + id: orderId, + products: req.products, + totalPrice, + }; + return { + isBase64Encoded: false, + statusCode: 200, + headers: responseHeaders, + body: JSON.stringify(order), + }; + } catch (error) { + return { + isBase64Encoded: false, + statusCode: 500, + headers: responseHeaders, + body: error.message, + }; + } +}; + +exports.finish = async ( + event: APIGatewayEvent, + context: Context, + callback: Callback, +) => { + try { + const req: FinishOrderRequest = JSON.parse(event.body); + + if (!req.orderId) + return { + isBase64Encoded: false, + statusCode: 400, + headers: responseHeaders, + body: 'orderId must be provided', + }; + if (!req.restaurantId) + return { + isBase64Encoded: false, + statusCode: 400, + headers: responseHeaders, + body: 'restaurantId must be provided', + }; + + const command = new UpdateCommand({ + TableName: 'wakeup-challenge-orders', + Key: { + restaurantId: req.restaurantId, + }, + UpdateExpression: 'set orderStatus = :orderStatus', + ConditionExpression: 'orderId = :orderId', + ExpressionAttributeValues: { + ':orderStatus': 'finished', + ':orderId': req.orderId, + }, + ReturnValues: 'ALL_NEW', + }); + const response = await docClient.send(command) as Omit & { Item: Order }; + console.log('response: ', response); + + const updatedItem = response.Attributes; + const updatedOrder: OrderDTO = { + id: updatedItem.orderId, + products: updatedItem.products, + totalPrice: updatedItem.totalPrice, + }; + + return { + isBase64Encoded: false, + statusCode: 200, + headers: responseHeaders, + body: JSON.stringify(updatedOrder), + }; + } catch (error) { + return { + isBase64Encoded: false, + statusCode: 500, + headers: responseHeaders, + body: error.message, + }; + } +}; diff --git a/packages/api/src/products.ts b/packages/api/src/products.ts index 477bb52..4bb601c 100644 --- a/packages/api/src/products.ts +++ b/packages/api/src/products.ts @@ -1,9 +1,7 @@ import { APIGatewayEvent, Callback, Context } from 'aws-lambda'; -import { CreateOrderRequest, Order, Product } from './types'; +import { ProductDTO } from './types'; import { - BatchWriteItemCommand, DynamoDBClient, - WriteRequest, } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb'; @@ -33,9 +31,7 @@ exports.get = async ( }; const lastKey = req.lastKey ? JSON.parse(req.lastKey) : undefined; - const limit = req.limit ? Number(req.limit) : 5; - const offset = req.offset ? Number(req.offset) : 0; const command = new QueryCommand({ ExpressionAttributeValues: { @@ -49,35 +45,13 @@ exports.get = async ( const response = await docClient.send(command); console.log('response: ', response); - const productsDB: Product[] = response.Items.map((x) => ({ + const products: ProductDTO[] = response.Items.map((x) => ({ id: x.productId, name: x.name, price: x.price, })); const newLastKey = response.LastEvaluatedKey; - // const productsDB: Product[] = [ - // { id: 1, name: 'Kuva Roast Rib Eye', price: 418 }, - // { id: 2, name: 'Guadalupe Half Rack', price: 298 }, - // { id: 3, name: 'Tohono Chicken', price: 308 }, - // { id: 4, name: 'Milanesa napolitana', price: 500 }, - // { id: 5, name: 'Asado', price: 800 }, - // { id: 6, name: 'Choripan', price: 230 }, - // { id: 7, name: 'Asado x4', price: 2700 }, - // { id: 8, name: 'Kuva Roast Rib Eye', price: 418 }, - // { id: 9, name: 'Guadalupe Half Rack', price: 298 }, - // { id: 10, name: 'Tohono Chicken', price: 308 }, - // { id: 11, name: 'Milanesa napolitana', price: 500 }, - // { id: 12, name: 'Asado', price: 800 }, - // { id: 13, name: 'Choripan', price: 230 }, - // { id: 14, name: 'Asado x45', price: 2700 }, - // { id: 15, name: 'Asado', price: 800 }, - // { id: 16, name: 'Choripan', price: 230 }, - // { id: 17, name: 'Asado x455', price: 2700 }, - // ]; - - const products = productsDB.slice(offset, offset + limit); - return { isBase64Encoded: false, statusCode: 200, @@ -93,39 +67,3 @@ exports.get = async ( }; } }; - -exports.createOrder = async ( - event: APIGatewayEvent, - context: Context, - callback: Callback, -) => { - try { - const req: CreateOrderRequest = JSON.parse(event.body); - - if (!req?.products) - return { - isBase64Encoded: false, - statusCode: 400, - headers: responseHeaders, - body: 'Products must be provided', - }; - const order: Order = { - products: req.products, - totalPrice: req.products.reduce((acc, val) => acc + val.product.price, 0), - }; - - return { - isBase64Encoded: false, - statusCode: 200, - headers: responseHeaders, - body: JSON.stringify(order), - }; - } catch (error) { - return { - isBase64Encoded: false, - statusCode: 500, - headers: responseHeaders, - body: error.message, - }; - } -}; diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 66c5ad0..0cd6f36 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,18 +1,25 @@ export interface CreateOrderRequest { + restaurantId: number; products: OrderProduct[]; } + +export interface FinishOrderRequest { + orderId: string; + restaurantId: number; +} export interface OrderProduct { - product: Product; + product: ProductDTO; amount: number; } -export interface Product { +export interface ProductDTO { id: number; name: string; price: number; } -export interface Order { +export interface OrderDTO { + id: string; products: OrderProduct[]; totalPrice: number; } @@ -21,3 +28,19 @@ export interface Restaurant { id: number; name: string; } + +//DB Types +export interface Order { + totalPrice: number; + orderStatus: string; + products: OrderProduct[]; + orderId: string; + restaurantId: number; +} + +export interface Product { + restaurantId: number; + productId: number; + name: string; + price: number; +} diff --git a/packages/api/template.yaml b/packages/api/template.yaml index e399939..d4f7250 100644 --- a/packages/api/template.yaml +++ b/packages/api/template.yaml @@ -36,7 +36,7 @@ Resources: Type: AWS::Serverless::Function Properties: CodeUri: ./dist - Handler: products.createOrder + Handler: orders.create Runtime: nodejs20.x MemorySize: 128 Events: @@ -46,6 +46,36 @@ Resources: Method: POST Path: /createOrder RestApiId: !Ref MyApi + + FinishOrderFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./dist + Handler: orders.finish + Runtime: nodejs20.x + MemorySize: 128 + Events: + Event: + Type: Api + Properties: + Method: POST + Path: /finishOrder + RestApiId: !Ref MyApi + + GetOrdersFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./dist + Handler: orders.get + Runtime: nodejs20.x + MemorySize: 128 + Events: + Event: + Type: Api + Properties: + Method: GET + Path: /orders + RestApiId: !Ref MyApi GetRestaurantsFunction: Type: AWS::Serverless::Function diff --git a/packages/front/src/components/app/hooks/useCreateOrder.ts b/packages/front/src/components/app/hooks/useCreateOrder.ts index 27555fd..1edb876 100644 --- a/packages/front/src/components/app/hooks/useCreateOrder.ts +++ b/packages/front/src/components/app/hooks/useCreateOrder.ts @@ -11,7 +11,7 @@ export interface OrderProduct { amount: number; } export interface CreateOrderRequest { - products: OrderProduct[]; + restaurantId: number; } const useCreateOrder = () => { @@ -27,12 +27,13 @@ const useCreateOrder = () => { .concat(productToAdd ? [productToAdd] : []), ]); }; - const createOrder = () => { + const createOrder = ({restaurantId}:CreateOrderRequest) => { if (orderProducts.length > 0) { axios .post( `${process.env.REACT_APP_API_ENDPOINT}/createOrder`, { + restaurantId, products: orderProducts, }, { diff --git a/packages/front/src/pages/Products.tsx b/packages/front/src/pages/Products.tsx index bcd3d5c..793faf1 100644 --- a/packages/front/src/pages/Products.tsx +++ b/packages/front/src/pages/Products.tsx @@ -26,7 +26,7 @@ const Products = () => { useCreateOrder(); const onCreateOrder = () => { - createOrder(); + createOrder({ restaurantId }); setIsSuccessModalOpen(true); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d022f0f..6e1308f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: '@aws-sdk/lib-dynamodb': specifier: ^3.577.0 version: 3.577.0(@aws-sdk/client-dynamodb@3.577.0) + uuid: + specifier: ^9.0.1 + version: 9.0.1 devDependencies: '@types/aws-lambda': specifier: ^8.10.137