Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.x.x lambda todos #56

Merged
merged 5 commits into from
Jan 29, 2024
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Examples converted to Hummingbird 2.0
- [http2](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/http2) - Basic application with HTTP2 upgrade added.
- [sessions](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/sessions) - Username/password and session authentication.
- [todos-dynamodb](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-dynamodb) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB.
- [todos-lambda](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-lambda) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB and running on AWS Lambda.
- [todos-mongokitten-openapi](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-mongokitten-openapi) - Todos application, using MongoDB driver [MongoKitten](https://github.com/orlandos-nl/MongoKitten) and the [OpenAPI runtime](https://github.com/apple/swift-openapi-runtime).
- [todos-postgres-tutorial](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-postgres-tutorial) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using PostgresNIO. Sample code that goes along with the [Todos tutorial](https://hummingbird-project.github.io/hummingbird-docs/2.0/tutorials/todos).
- [upload](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/upload) - File uploading and downloading.
Expand All @@ -22,7 +23,6 @@ Examples still working with Hummingbird 1.0
- [proxy-server](https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server) - Using AsyncHTTPClient to build a proxy server
- [proxy-server-core](https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server-core) - Version of proxy server only using HummingbirdCore
- [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent
- [todos-lambda](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-lambda) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB and running on AWS Lambda.
- [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/main/upload-s3) - File uploading and downloading using AWS S3 as backing store.
- [websocket-chat](https://github.com/hummingbird-project/hummingbird-examples/tree/main/websocket-chat) - Simple chat application using WebSockets.
- [websocket-echo](https://github.com/hummingbird-project/hummingbird-examples/tree/main/websocket-echo) - Simple WebSocket based echo server.
Expand Down
Binary file modified auth-cognito/auth-cognito-test.paw
Binary file not shown.
4 changes: 2 additions & 2 deletions todos-dynamodb/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Todos DynamoDB using Async/Await
# Todos DynamoDB

This is an implementation of the [TodoBackend](http://www.todobackend.com/) API using DynamoDB to store the todo data. This sample uses the async/await APIs of Hummingbird. It has six routes
This is an implementation of the [TodoBackend](http://www.todobackend.com/) API using DynamoDB to store the todo data. It has six routes

- GET /todos: Lists all the todos in the database
- POST /todos: Creates a new todo
Expand Down
14 changes: 5 additions & 9 deletions todos-lambda/Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
// swift-tools-version:5.7
// swift-tools-version:5.9

import PackageDescription

let package = Package(
name: "hummingbird-todos-lambda",
platforms: [
.macOS(.v12),
],
products: [
.executable(name: "HummingbirdTodosLambda", targets: ["App"]),
.macOS(.v14),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-lambda.git", from: "1.0.0-rc.3"),
.package(url: "https://github.com/soto-project/soto.git", from: "6.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-alpha.1"),
.package(url: "https://github.com/hummingbird-project/hummingbird-lambda.git", from: "2.0.0-alpha.2"),
.package(url: "https://github.com/soto-project/soto.git", from: "7.0.0-alpha"),
],
targets: [
.executableTarget(name: "App", dependencies: [
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
.product(name: "HummingbirdLambda", package: "hummingbird-lambda"),
.product(name: "SotoDynamoDB", package: "soto"),
]),
Expand Down
16 changes: 16 additions & 0 deletions todos-lambda/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Todos Lambda

This is an implementation of the [TodoBackend](http://www.todobackend.com/) API using HummingbirdLambda to run on an AWS Lambda. It uses DynamoDB to store the todo data. It has six routes

- GET /todos: Lists all the todos in the database
- POST /todos: Creates a new todo
- DELETE /todos: Deletes all the todos
- GET /todos/:id : Returns a single todo with id
- PATCH /todos/:id : Edits todo with id
- DELETE /todos/:id : Deletes todo with id

A todo consists of a title, order number, url to link to edit/get/delete that todo and whether that todo is complete or not.

To test this example you will need an AWS account. The example uses AWS SAM to deploy the lambda, create the DynamoDB table and APIGateway for accessing the lambda API. Installation details for AWS SAM can be found in https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html. In the scripts folder there are scripts `build-and-package.sh` to build your lambda, and `deploy.sh` to deploy it to AWS.

This example comes with a [PAW](https://paw.cloud/) file you can use to test the various endpoints. You will need to edit the development environment to update the host URL to point to your lambda.
44 changes: 0 additions & 44 deletions todos-lambda/Sources/App/Application+configure.swift

This file was deleted.

119 changes: 52 additions & 67 deletions todos-lambda/Sources/App/Controllers/TodoController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -12,67 +12,63 @@
//
//===----------------------------------------------------------------------===//

import AWSLambdaEvents
import Foundation
import Hummingbird
import HummingbirdLambda
import NIO
import SotoDynamoDB

extension UUID: LosslessStringConvertible {
public init?(_ description: String) {
self.init(uuidString: description)
}
}

struct TodoController {
let tableName = "hummingbird-todos"
func addRoutes(to group: HBRouterGroup) {
typealias Context = HBBasicLambdaRequestContext<APIGatewayRequest>

let dynamoDB: DynamoDB
let tableName: String

func addRoutes(to group: HBRouterGroup<Context>) {
group
.post(use: self.create)
.get("{id}", use: self.get)
.get(use: self.list)
.post(options: .editResponse, use: self.create)
.patch("{id}", use: self.updateId)
.delete("{id}", use: self.deleteId)
.delete(use: self.deleteAll)
.get(":id", use: self.get)
.patch(":id", use: self.updateId)
.delete(":id", use: self.deleteId)
}

func list(_ request: HBRequest) -> EventLoopFuture<[Todo]> {
@Sendable func list(_ request: HBRequest, context: Context) async throws -> [Todo] {
let input = DynamoDB.ScanInput(tableName: self.tableName)
return request.aws.dynamoDB.scan(input, type: Todo.self, logger: request.logger, on: request.eventLoop)
.map { $0.items ?? [] }
let scanResponse = try await self.dynamoDB.scan(input, type: Todo.self, logger: context.logger)
return scanResponse.items ?? []
}

func create(_ request: HBRequest) -> EventLoopFuture<Todo> {
guard var todo = try? request.decode(as: Todo.self) else { return request.failure(HBHTTPError(.badRequest)) }
guard let host = request.headers["host"].first else { return request.failure(HBHTTPError(.badRequest, message: "No host header")) }
let path = request.apiGatewayRequest.requestContext.path
@Sendable func create(_ request: HBRequest, context: Context) async throws -> HBEditedResponse<Todo> {
var todo = try await request.decode(as: Todo.self, context: context)
guard let host = request.head.authority else { throw HBHTTPError(.badRequest, message: "No host header") }
let path = context.event.requestContext.path

todo.id = UUID()
todo.completed = false
todo.url = "https://\(host)\(path)/\(todo.id!)"
let input = DynamoDB.PutItemCodableInput(item: todo, tableName: self.tableName)
return request.aws.dynamoDB.putItem(input, logger: request.logger, on: request.eventLoop)
.map { _ in
request.response.status = .created
return todo
}
_ = try await self.dynamoDB.putItem(input, logger: context.logger)
return HBEditedResponse(status: .created, response: todo)
}

func get(_ request: HBRequest) -> EventLoopFuture<Todo?> {
guard let id = request.parameters.get("id", as: String.self) else { return request.failure(HBHTTPError(.badRequest)) }
@Sendable func get(_ request: HBRequest, context: Context) async throws -> Todo? {
let id = try context.parameters.require("id", as: String.self)
let input = DynamoDB.QueryInput(
consistentRead: true,
expressionAttributeValues: [":id": .s(id)],
keyConditionExpression: "id = :id",
tableName: self.tableName
)
return request.aws.dynamoDB.query(input, type: Todo.self, logger: request.logger, on: request.eventLoop)
.map { $0.items?.first }
let queryResponse = try await self.dynamoDB.query(input, type: Todo.self, logger: context.logger)
return queryResponse.items?.first
}

func updateId(_ request: HBRequest) -> EventLoopFuture<Todo> {
guard var todo = try? request.decode(as: EditTodo.self) else { return request.failure(HBHTTPError(.badRequest)) }
guard let id = request.parameters.get("id", as: UUID.self) else { return request.failure(HBHTTPError(.badRequest)) }
@Sendable func updateId(_ request: HBRequest, context: Context) async throws -> Todo {
var todo = try await request.decode(as: EditTodo.self, context: context)
let id = try context.parameters.require("id", as: UUID.self)
todo.id = id
let input = DynamoDB.UpdateItemCodableInput(
conditionExpression: "attribute_exists(id)",
Expand All @@ -81,51 +77,40 @@ struct TodoController {
tableName: self.tableName,
updateItem: todo
)
return request.aws.dynamoDB.updateItem(input, logger: request.logger, on: request.eventLoop)
.flatMapErrorThrowing { error in
if let error = error as? DynamoDBErrorType, error == .conditionalCheckFailedException {
throw HBHTTPError(.notFound)
}
throw error
}
.flatMapThrowing { response in
guard let attributes = response.attributes else { throw HBHTTPError(.internalServerError) }
return try DynamoDBDecoder().decode(Todo.self, from: attributes)
}
do {
let response = try await self.dynamoDB.updateItem(input, logger: context.logger)
guard let attributes = response.attributes else { throw HBHTTPError(.internalServerError) }
return try DynamoDBDecoder().decode(Todo.self, from: attributes)
} catch let error as DynamoDBErrorType where error == .conditionalCheckFailedException {
throw HBHTTPError(.notFound)
}
}

func deleteAll(_ request: HBRequest) -> EventLoopFuture<HTTPResponseStatus> {
@Sendable func deleteAll(_ request: HBRequest, context: Context) async throws -> HTTPResponse.Status {
let input = DynamoDB.ScanInput(tableName: self.tableName)
return request.aws.dynamoDB.scan(input, logger: request.logger, on: request.eventLoop)
.map(\.items)
.unwrap(orReplace: [])
.flatMap { items -> EventLoopFuture<Void> in
let requestItems: [DynamoDB.WriteRequest] = items.compactMap { item in
item["id"].map { .init(deleteRequest: .init(key: ["id": $0])) }
}
guard requestItems.count > 0 else { return request.success(()) }
let input = DynamoDB.BatchWriteItemInput(requestItems: [self.tableName: requestItems])
return request.aws.dynamoDB.batchWriteItem(input, logger: request.logger, on: request.eventLoop)
.map { _ in }
}
.map { _ in .ok }
let items = try await self.dynamoDB.scan(input, logger: context.logger).items ?? []
let requestItems: [DynamoDB.WriteRequest] = items.compactMap { item in
item["id"].map { .init(deleteRequest: .init(key: ["id": $0])) }
}
guard requestItems.count > 0 else { return .ok }
let batchWriteInput = DynamoDB.BatchWriteItemInput(requestItems: [self.tableName: requestItems])
_ = try await self.dynamoDB.batchWriteItem(batchWriteInput, logger: context.logger)
return .ok
}

func deleteId(_ request: HBRequest) -> EventLoopFuture<HTTPResponseStatus> {
guard let id = request.parameters.get("id", as: String.self) else { return request.failure(HBHTTPError(.badRequest)) }
@Sendable func deleteId(_ request: HBRequest, context: Context) async throws -> HTTPResponse.Status {
let id = try context.parameters.require("id", as: String.self)

let input = DynamoDB.DeleteItemInput(
conditionExpression: "attribute_exists(id)",
key: ["id": .s(id)],
tableName: self.tableName
)
return request.aws.dynamoDB.deleteItem(input, logger: request.logger, on: request.eventLoop)
.flatMapErrorThrowing { error in
if let error = error as? DynamoDBErrorType, error == .conditionalCheckFailedException {
throw HBHTTPError(.notFound)
}
throw error
}
.map { _ in .ok }
do {
_ = try await self.dynamoDB.deleteItem(input, logger: context.logger)
return .ok
} catch let error as DynamoDBErrorType where error == .conditionalCheckFailedException {
throw HBHTTPError(.notFound)
}
}
}
48 changes: 0 additions & 48 deletions todos-lambda/Sources/App/Hummingbird+Soto.swift

This file was deleted.

2 changes: 1 addition & 1 deletion todos-lambda/Sources/App/Models/Todo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down
Loading