def lambda_handler(event, context): """ Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user is logged in. """ cart_id, _ = get_cart_id(event["headers"]) try: # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here user_id = event["requestContext"]["authorizer"]["claims"]["sub"] except KeyError: return { "statusCode": 400, "headers": get_headers(cart_id), "body": json.dumps({"message": "Invalid user"}), } # Get all cart items belonging to the user's identity response = table.query( KeyConditionExpression=Key("pk").eq(f"user#{user_id}") & Key("sk").begins_with("product#"), ConsistentRead= True, # Perform a strongly consistent read here to ensure we get correct and up to date cart ) cart_items = response.get("Items") # batch_writer will be used to update status for cart entries belonging to the user with table.batch_writer() as batch: for item in cart_items: # Delete ordered items batch.delete_item(Key={"pk": item["pk"], "sk": item["sk"]}) metrics.add_metric(name="CartCheckedOut", unit="Count", value=1) logger.info({"action": "CartCheckedOut", "cartItems": cart_items}) return { "statusCode": 200, "headers": get_headers(cart_id), "body": json.dumps({"products": response.get("Items")}, default=handle_decimal_type), }
def lambda_handler(event, context): """ List items in shopping cart. """ cart_id, generated = get_cart_id(event["headers"]) # Because this method can be called anonymously, we need to check there's a logged in user jwt_token = event["headers"].get("Authorization") if jwt_token: user_sub = get_user_sub(jwt_token) key_string = f"user#{user_sub}" logger.structure_logs(append=True, cart_id=f"user#{user_sub}") else: key_string = f"cart#{cart_id}" logger.structure_logs(append=True, cart_id=f"cart#{cart_id}") # No need to query database if the cart_id was generated rather than passed into the function if generated: logger.info( "cart ID was generated in this request, not fetching cart from DB") product_list = [] else: logger.info("Fetching cart from DB") response = table.query( KeyConditionExpression=Key("pk").eq(key_string) & Key("sk").begins_with("product#"), ProjectionExpression="sk,quantity,productDetail", FilterExpression= "quantity > :val", # Only return items with more than 0 quantity ExpressionAttributeValues={":val": 0}, ) product_list = response.get("Items", []) for product in product_list: product.update((k, v.replace("product#", "")) for k, v in product.items() if k == "sk") return { "statusCode": 200, "headers": get_headers(cart_id), "body": json.dumps({"products": product_list}, default=handle_decimal_type), }
def lambda_handler(event, context): """ Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user is logged in. """ logger.debug(event) cart_id, _ = get_cart_id(event["headers"]) try: # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here user_id = event["requestContext"]["authorizer"]["claims"]["sub"] except KeyError: return { "statusCode": 400, "headers": get_headers(cart_id), "body": json.dumps({"message": "Invalid user"}), } # Get all cart items belonging to the user's anonymous identity response = table.query( KeyConditionExpression=Key("pk").eq(f"cart#{cart_id}") & Key("sk").begins_with("product#")) unauth_cart = response["Items"] # Since there's no batch operation available for updating items, and there's no dependency between them, we can # run them in parallel threads. thread_list = [] for item in unauth_cart: # Store items with user identifier as pk instead of "unauthenticated" cart ID # Using threading library to perform updates in parallel ddb_updateitem_thread = threading.Thread(target=update_item, args=(user_id, item)) thread_list.append(ddb_updateitem_thread) ddb_updateitem_thread.start() # Delete items with unauthenticated cart ID # Rather than deleting directly, push to SQS queue to handle asynchronously queue.send_message( MessageBody=json.dumps(item, default=handle_decimal_type)) for ddb_thread in thread_list: ddb_thread.join() # Block main thread until all updates finished response = table.query( KeyConditionExpression=Key("pk").eq(f"user#{user_id}") & Key("sk").begins_with("product#"), ProjectionExpression="sk,quantity,productDetail", ConsistentRead= True, # Perform a strongly consistent read here to ensure we get correct values after updates ) product_list = response.get("Items", []) for product in product_list: product.update((k, v.replace("product#", "")) for k, v in product.items() if k == "sk") return { "statusCode": 200, "headers": get_headers(cart_id), "body": json.dumps({"products": product_list}, default=handle_decimal_type), }
def lambda_handler(event, context): """ Idempotent update quantity of products in a cart. Quantity provided will overwrite existing quantity for a specific product in cart, rather than adding to it. """ try: request_payload = json.loads(event["body"]) except KeyError: return { "statusCode": 400, "headers": get_headers(), "body": json.dumps({"message": "No Request payload"}), } # retrieve the product_id that was specified in the url product_id = event["pathParameters"]["product_id"] quantity = int(request_payload["quantity"]) cart_id, _ = get_cart_id(event["headers"]) # Because this method can be called anonymously, we need to check if there's a logged in user user_sub = None jwt_token = event["headers"].get("Authorization") if jwt_token: user_sub = get_user_sub(jwt_token) try: product = get_product_from_external_service(product_id) except NotFoundException: logger.info("No product found with product_id: %s", product_id) return { "statusCode": 404, "headers": get_headers(cart_id=cart_id), "body": json.dumps({"message": "product not found"}), } # Prevent storing negative quantities of things if quantity < 0: return { "statusCode": 400, "headers": get_headers(cart_id), "body": json.dumps( { "productId": product_id, "message": "Quantity must not be lower than 0", } ), } # Use logged in user's identifier if it exists, otherwise use the anonymous identifier if user_sub: pk = f"user#{user_sub}" ttl = generate_ttl( 7 ) # Set a longer ttl for logged in users - we want to keep their cart for longer. else: pk = f"cart#{cart_id}" ttl = generate_ttl() table.put_item( Item={ "pk": pk, "sk": f"product#{product_id}", "quantity": quantity, "expirationTime": ttl, "productDetail": product, } ) logger.info("about to add metrics...") metrics.add_metric(name="CartUpdated", unit="Count", value=1) return { "statusCode": 200, "headers": get_headers(cart_id), "body": json.dumps( {"productId": product_id, "quantity": quantity, "message": "cart updated"} ), }
def lambda_handler(event, context): """ Add a the provided quantity of a product to a cart. Where an item already exists in the cart, the quantities will be summed. """ logger.debug(event) try: request_payload = json.loads(event["body"]) except KeyError: return { "statusCode": 400, "headers": get_headers(), "body": json.dumps({"message": "No Request payload"}), } product_id = request_payload["productId"] quantity = request_payload.get("quantity", 1) cart_id, _ = get_cart_id(event["headers"]) # Because this method can be called anonymously, we need to check there's a logged in user user_sub = None jwt_token = event["headers"].get("Authorization") if jwt_token: user_sub = get_user_sub(jwt_token) try: product = get_product_from_external_service(product_id) except NotFoundException: return { "statusCode": 404, "headers": get_headers(cart_id=cart_id), "body": json.dumps({"message": "product not found"}), } if user_sub: pk = f"user#{user_sub}" ttl = generate_ttl( 7 ) # Set a longer ttl for logged in users - we want to keep their cart for longer. else: pk = f"cart#{cart_id}" ttl = generate_ttl() if int(quantity) < 0: table.update_item( Key={ "pk": pk, "sk": f"product#{product_id}" }, ExpressionAttributeNames={ "#quantity": "quantity", "#expirationTime": "expirationTime", "#productDetail": "productDetail", }, ExpressionAttributeValues={ ":val": quantity, ":ttl": ttl, ":productDetail": product, ":limit": abs(quantity), }, UpdateExpression= "ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail", # Prevent quantity less than 0 ConditionExpression="quantity >= :limit", ) else: table.update_item( Key={ "pk": pk, "sk": f"product#{product_id}" }, ExpressionAttributeNames={ "#quantity": "quantity", "#expirationTime": "expirationTime", "#productDetail": "productDetail", }, ExpressionAttributeValues={ ":val": quantity, ":ttl": generate_ttl(), ":productDetail": product, }, UpdateExpression= "ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail", ) return { "statusCode": 200, "headers": get_headers(cart_id), "body": json.dumps({ "productId": product_id, "message": "product added to cart" }), }
def lambda_handler(event, context): """ API Handler The API catch the request and builds a record in dynamodb for each request with sender's IP, build and id and timestamp """ logger.info(event) logger.info(f"Table is {tableName}") # Controls body try: request_payload = json.loads(event["body"]) except: return { "statusCode": 400, "body": json.dumps({"message": "No Request payload"}), } # Controls payload : clientId is mandatory` client_id = "" try: client_id = request_payload["client_id"] except KeyError: return { "statusCode": 400, "headers": get_headers(client_id), "body": json.dumps({"message": "No client_id in payload ?"}), } # Build the record ip = requests.get("http://checkip.amazonaws.com/").text.replace("\n", "") pk = str(uuid.uuid4()) timestamp = time.strftime('%Y-%m-%d %H:%M:%S') week = datetime.datetime.today() + datetime.timedelta(days=7) expiry_date_time = int(time.mktime(week.timetuple())) try: result = dynamodb.put_item( TableName=tableName, Item={ 'pk': {'S': pk}, 'sk': {'S': ip}, 'client_id': {'S': client_id}, 'timestamp': {'S': timestamp}, 'ttl': {'N': str(expiry_date_time)} }) print(result) except Exception as e: logger.error(e) # result = table.update_item( # Key={ # "pk": {"S": pk} # }, # ExpressionAttributeValues={ # ":ip": {"S": ip}, # ":ttl": {"N": ttl}, # ":client_id": {"S": client_id}, # ":timestamp": {"S": timestamp} # }, # UpdateExpression="SET sk=:ip, ttl=:ttl, client_id=:client_id, timestamp=:timestamp", # ReturnValues="ALL_NEW" # ) # metrics.add_metric(name="GreetingAdded", unit="Count", value=1) return { "statusCode": 200, "headers": get_headers(pk), "body": json.dumps( {"client_id": client_id, "ip": ip, "message": "Greetings added !"} ), }
def test_lambda_handler(self): """ API Handler tester The API catch the request and builds a record in dynamodb for each request with sender's IP, build and id and timestamp """ # Controls body try: request_payload = json.loads(self.event["body"]) except KeyError or ValueError: return { "statusCode": 400, "body": json.dumps({"message": "No Request payload"}), } # Controls payload : clientId is mandatory` client_id = "" try: client_id = request_payload["client_id"] except KeyError: return { "statusCode": 400, "headers": get_headers(client_id), "body": json.dumps({"message": "No client_id in payload ?"}), } # Build the record ip = requests.get("http://checkip.amazonaws.com/").text.replace("\n", "") pk = str(uuid.uuid4()) timestamp = time.strftime('%Y-%m-%d %H:%M:%S') ttl = str(generate_ttl(7)) result = self.dynamodb.update_item( TableName=self.tableName, Key={ "pk": {"S": pk}, "sk": {"S": f"ip#{ip}"} }, ExpressionAttributeNames={ "#ip": "ip", "#ttl": "ttl", "#client_id": "client_id", "#timestamp": "timestamp" }, ExpressionAttributeValues={ ":ip": {"S": ip}, ":ttl": {"N": ttl}, ":client_id": {"S": client_id}, ":timestamp": {"S": timestamp} }, UpdateExpression="SET #ttl=:ttl, #client_id=:client_id, #timestamp=:timestamp", ReturnValues="ALL_NEW" ) pprint(result) # metrics.add_metric(name="GreetingAdded", unit="Count", value=1) response = { "statusCode": 200, "headers": get_headers(pk), "body": json.dumps( {"client_id": client_id, "ip": ip, "message": "Greetings added !"} ), } pprint(response) self.assertEqual(response['statusCode'], 200)