From 4974b6c24e32f9dfeac3756f63cb88d1cd3c855b Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Fri, 12 Jun 2020 23:22:39 +0200 Subject: [PATCH 01/10] Add LambdaAPI Example of a Serverless REST API --- Examples/LambdaFunctions/Package.swift | 15 ++ .../LambdaAPI/APIGateway+Extensions.swift | 73 ++++++ .../Sources/LambdaAPI/Product+DynamoDB.swift | 53 +++++ .../Sources/LambdaAPI/Product.swift | 24 ++ .../Sources/LambdaAPI/ProductLambda.swift | 106 +++++++++ .../LambdaAPI/ProductLambdaHandler.swift | 222 ++++++++++++++++++ .../Sources/LambdaAPI/ProductService.swift | 104 ++++++++ .../Sources/LambdaAPI/main.swift | 20 ++ .../Sources/LambdaAPI/swagger.json | 202 ++++++++++++++++ .../scripts/SAM/LambdaAPI-template.yml | 105 +++++++++ .../scripts/serverless/LambdaAPI-template.yml | 96 ++++++++ 11 files changed, 1020 insertions(+) create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/APIGateway+Extensions.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/Product+DynamoDB.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/Product.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambdaHandler.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/ProductService.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/main.swift create mode 100644 Examples/LambdaFunctions/Sources/LambdaAPI/swagger.json create mode 100644 Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml create mode 100644 Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml diff --git a/Examples/LambdaFunctions/Package.swift b/Examples/LambdaFunctions/Package.swift index ae79d287..e3220a35 100644 --- a/Examples/LambdaFunctions/Package.swift +++ b/Examples/LambdaFunctions/Package.swift @@ -18,12 +18,20 @@ let package = Package( .executable(name: "APIGateway", targets: ["APIGateway"]), // fully featured example with domain specific business logic .executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]), + // Full REST API Example using APIGateway, Lambda, DynamoDB + .executable(name: "LambdaAPI", targets: ["LambdaAPI"]), ], dependencies: [ // this is the dependency on the swift-aws-lambda-runtime library // in real-world projects this would say // .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0") .package(name: "swift-aws-lambda-runtime", path: "../.."), + + // The following packages are required by LambdaAPI + // AWS SDK Swift + .package(url: "https://github.com/swift-aws/aws-sdk-swift.git", from: "5.0.0-alpha.3"), + // Logging + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], targets: [ .target(name: "HelloWorld", dependencies: [ @@ -42,5 +50,12 @@ let package = Package( .target(name: "CurrencyExchange", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), ]), + .target(name: "LambdaAPI", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), + .product(name: "AWSDynamoDB", package: "aws-sdk-swift"), + .product(name: "Logging", package: "swift-log"), + ] + ) ] ) diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/APIGateway+Extensions.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/APIGateway+Extensions.swift new file mode 100644 index 00000000..b7040eed --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/APIGateway+Extensions.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import AWSLambdaEvents + +public extension APIGateway.V2.Request { + func object() throws -> T { + let decoder = JSONDecoder() + guard let body = self.body, + let dataBody = body.data(using: .utf8) else { + throw APIError.invalidRequest + } + return try decoder.decode(T.self, from: dataBody) + } +} + +let defaultHeaders = ["Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE", + "Access-Control-Allow-Credentials": "true"] + +extension APIGateway.V2.Response { + + init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { + + self.init( + statusCode: statusCode, + headers: defaultHeaders, + multiValueHeaders: nil, + body: "{\"message\":\"\(error.localizedDescription)\"}", + isBase64Encoded: false + ) + } + + init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) { + let encoder = JSONEncoder() + + var body: String = "{}" + if let data = try? encoder.encode(object) { + body = String(data: data, encoding: .utf8) ?? body + } + self.init( + statusCode: statusCode, + headers: defaultHeaders, + multiValueHeaders: nil, + body: body, + isBase64Encoded: false + ) + + } + + init(with result: Result, statusCode: AWSLambdaEvents.HTTPResponseStatus) { + + switch result { + case .success(let value): + self.init(with: value, statusCode: statusCode) + case .failure(let error): + self.init(with: error, statusCode: statusCode) + } + } +} diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/Product+DynamoDB.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/Product+DynamoDB.swift new file mode 100644 index 00000000..bd3bd7ea --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/Product+DynamoDB.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import AWSDynamoDB + +public struct ProductField { + static let sku = "sku" + static let name = "name" + static let description = "description" + static let createdAt = "createdAt" + static let updatedAt = "updatedAt" +} + +public extension Product { + var dynamoDictionary: [String : DynamoDB.AttributeValue] { + var dictionary = [ProductField.sku: DynamoDB.AttributeValue(s:sku), + ProductField.name: DynamoDB.AttributeValue(s:name), + ProductField.description: DynamoDB.AttributeValue(s:description)] + if let createdAt = createdAt { + dictionary[ProductField.createdAt] = DynamoDB.AttributeValue(s:createdAt) + } + + if let updatedAt = updatedAt { + dictionary[ProductField.updatedAt] = DynamoDB.AttributeValue(s:updatedAt) + } + return dictionary + } + + init(dictionary: [String: DynamoDB.AttributeValue]) throws { + guard let name = dictionary[ProductField.name]?.s, + let sku = dictionary[ProductField.sku]?.s, + let description = dictionary[ProductField.description]?.s else { + throw APIError.invalidItem + } + self.name = name + self.sku = sku + self.description = description + self.createdAt = dictionary[ProductField.createdAt]?.s + self.updatedAt = dictionary[ProductField.updatedAt]?.s + } +} diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/Product.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/Product.swift new file mode 100644 index 00000000..4ec5b1d4 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/Product.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct Product: Codable { + public let sku: String + public let name: String + public let description: String + public var createdAt: String? + public var updatedAt: String? +} + diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift new file mode 100644 index 00000000..6eb41355 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import AWSLambdaRuntime +import AWSDynamoDB +import NIO +import Logging +import AsyncHTTPClient +import AWSLambdaEvents + +enum Operation: String { + case create + case read + case update + case delete + case list + case unknown +} + +struct EmptyResponse: Codable { + +} + +struct ProductLambda: LambdaHandler { + + //typealias In = APIGateway.SimpleRequest + typealias In = APIGateway.V2.Request + typealias Out = APIGateway.V2.Response + + let dbTimeout:Int64 = 30 + + let region: Region + let db: AWSDynamoDB.DynamoDB + let service: ProductService + let tableName: String + let operation: Operation + + var httpClient: HTTPClient + + static func currentRegion() -> Region { + + if let awsRegion = ProcessInfo.processInfo.environment["AWS_REGION"] { + let value = Region(rawValue: awsRegion) + return value + + } else { + //Default configuration + return .useast1 + } + } + + static func tableName() throws -> String { + guard let tableName = ProcessInfo.processInfo.environment["PRODUCTS_TABLE_NAME"] else { + throw APIError.tableNameNotFound + } + return tableName + } + + init(eventLoop: EventLoop) { + + let handler = Lambda.env("_HANDLER") ?? "" + self.operation = Operation(rawValue: handler) ?? .unknown + + self.region = Self.currentRegion() + logger.info("\(Self.currentRegion())") + + let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout) + let timeout = HTTPClient.Configuration.Timeout(connect: lambdaRuntimeTimeout, + read: lambdaRuntimeTimeout) + let configuration = HTTPClient.Configuration(timeout: timeout) + self.httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: configuration) + + self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient)) + logger.info("DynamoDB") + + self.tableName = (try? Self.tableName()) ?? "" + + self.service = ProductService( + db: db, + tableName: tableName + ) + logger.info("ProductService") + } + + func handle(context: Lambda.Context, event: APIGateway.V2.Request, callback: @escaping (Result) -> Void) { + let _ = ProductLambdaHandler(service: service, operation: operation).handle(context: context, event: event) + .always { (result) in + callback(result) + } + } +} diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambdaHandler.swift new file mode 100644 index 00000000..82e2a4f2 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambdaHandler.swift @@ -0,0 +1,222 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import AWSLambdaRuntime +import NIO +import Logging +import AWSLambdaEvents + +struct ProductLambdaHandler: EventLoopLambdaHandler { + + typealias In = APIGateway.V2.Request + typealias Out = APIGateway.V2.Response + + let service: ProductService + let operation: Operation + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + + switch operation { + case .create: + logger.info("create") + let create = CreateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .created) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) + } + } + return create + case .read: + logger.info("read") + let read = ReadLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) + } + } + return read + case .update: + logger.info("update") + let update = UpdateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) + } + } + return update + case .delete: + logger.info("delete") + let delete = DeleteUpdateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) + } + } + return delete + case .list: + logger.info("list") + let list = ListUpdateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) + } + } + return list + case .unknown: + logger.info("unknown") + let value = APIGateway.V2.Response(with: APIError.handlerNotFound, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) + } + } + + struct CreateLambdaHandler { + + let service: ProductService + + init(service: ProductService) { + self.service = service + } + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { + + guard let product: Product = try? event.object() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.createItem(product: product) + .flatMapThrowing { item -> Result in + return Result.success(product) + } + return future + } + } + + struct ReadLambdaHandler { + + let service: ProductService + + init(service: ProductService) { + self.service = service + } + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { + + guard let sku = event.pathParameters?["sku"] else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.readItem(key: sku) + .flatMapThrowing { data -> Result in + let product = try Product(dictionary: data.item ?? [:]) + return Result.success(product) + } + return future + } + } + + struct UpdateLambdaHandler { + + let service: ProductService + + init(service: ProductService) { + self.service = service + } + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { + + guard let product: Product = try? event.object() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.updateItem(product: product) + .flatMapThrowing { (data) -> Result in + return Result.success(product) + } + return future + } + } + + struct DeleteUpdateLambdaHandler { + + let service: ProductService + + init(service: ProductService) { + self.service = service + } + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { + + guard let sku = event.pathParameters?["sku"] else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.deleteItem(key: sku) + .flatMapThrowing { (data) -> Result in + return Result.success(EmptyResponse()) + } + return future + } + } + + struct ListUpdateLambdaHandler { + + let service: ProductService + + init(service: ProductService) { + self.service = service + } + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { + + let future = service.listItems() + .flatMapThrowing { data -> Result<[Product],Error> in + let products: [Product]? = try data.items?.compactMap { (item) -> Product in + return try Product(dictionary: item) + } + let object = products ?? [] + return Result.success(object) + } + return future + } + } +} diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/ProductService.swift new file mode 100644 index 00000000..1e05a5a0 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/ProductService.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import AWSDynamoDB +import NIO + +public enum APIError: Error { + case invalidItem + case tableNameNotFound + case invalidRequest + case handlerNotFound +} + +extension Date { + var iso8601: String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter.string(from: self) + } +} + +public class ProductService { + + let db: DynamoDB + let tableName: String + + public init(db: DynamoDB, tableName: String) { + self.db = db + self.tableName = tableName + } + + public func createItem(product: Product) -> EventLoopFuture { + + var product = product + let date = Date().iso8601 + product.createdAt = date + product.updatedAt = date + + let input = DynamoDB.PutItemInput( + item: product.dynamoDictionary, + tableName: tableName + ) + return db.putItem(input) + } + + public func readItem(key: String) -> EventLoopFuture { + let input = DynamoDB.GetItemInput( + key: [ProductField.sku: DynamoDB.AttributeValue(s: key)], + tableName: tableName + ) + return db.getItem(input) + } + + public func updateItem(product: Product) -> EventLoopFuture { + + var product = product + let date = Date().iso8601 + product.updatedAt = date + + let input = DynamoDB.UpdateItemInput( + expressionAttributeNames: [ + "#name": ProductField.name, + "#description": ProductField.description, + "#updatedAt": ProductField.updatedAt + ], + expressionAttributeValues: [ + ":name": DynamoDB.AttributeValue(s:product.name), + ":description": DynamoDB.AttributeValue(s:product.description), + ":updatedAt": DynamoDB.AttributeValue(s:product.updatedAt) + ], + key: [ProductField.sku: DynamoDB.AttributeValue(s: product.sku)], + returnValues: DynamoDB.ReturnValue.allNew, + tableName: tableName, + updateExpression: "SET #name = :name, #description = :description, #updatedAt = :updatedAt" + ) + return db.updateItem(input) + } + + public func deleteItem(key: String) -> EventLoopFuture { + let input = DynamoDB.DeleteItemInput( + key: [ProductField.sku: DynamoDB.AttributeValue(s: key)], + tableName: tableName + ) + return db.deleteItem(input) + } + + public func listItems() -> EventLoopFuture { + let input = DynamoDB.ScanInput(tableName: tableName) + return db.scan(input) + } +} diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/main.swift b/Examples/LambdaFunctions/Sources/LambdaAPI/main.swift new file mode 100644 index 00000000..369a5397 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/main.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import Logging + +let logger = Logger(label: "AWS.Lambda.Products") + +Lambda.run(ProductLambda.init) diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/swagger.json b/Examples/LambdaFunctions/Sources/LambdaAPI/swagger.json new file mode 100644 index 00000000..76d4c54a --- /dev/null +++ b/Examples/LambdaFunctions/Sources/LambdaAPI/swagger.json @@ -0,0 +1,202 @@ +{ + "swagger": "2.0", + "info": { + "version": "2020-05-25T15:42:57Z", + "title": "lambda-api-swift", + "contact": { + "name": "swift-server", + "url": "https://github.com/swift-server/" + }, + "license": { + "name": "Apache 2.0" + }, + "description": "Product API - Demo" + }, + "host": "", + "basePath": "/dev", + "schemes": [ + "https" + ], + "paths": { + "/products": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Product" + } + } + } + }, + "description": "List Products", + "operationId": "get-products" + }, + "post": { + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/Product" + } + } + }, + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/Product" + } + } + ], + "description": "Create Product", + "operationId": "post-product" + }, + "put": { + "parameters": [ + { + "in": "body", + "name": "body", + "schema": { + "$ref": "#/definitions/Product" + }, + "description": "" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Product" + } + } + }, + "description": "Update Product", + "operationId": "put-product" + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + } + } + }, + "/products/{sku}": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Product" + } + } + }, + "parameters": [], + "description": "Get Product", + "operationId": "get-product" + }, + "delete": { + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "object", + "properties": {} + } + } + }, + "parameters": [], + "description": "Delete Product", + "operationId": "delete-product" + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + } + }, + "parameters": [ + { + "type": "string", + "name": "sku", + "in": "path", + "required": true + } + ] + } + }, + "definitions": { + "Product": { + "title": "Product", + "type": "object", + "x-examples": { + "example-1": { + "sku": "3", + "name": "Book", + "description": "Book 3" + } + }, + "properties": { + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "sku", + "name", + "description" + ] + } + } +} \ No newline at end of file diff --git a/Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml b/Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml new file mode 100644 index 00000000..6575dd03 --- /dev/null +++ b/Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml @@ -0,0 +1,105 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A sample SAM template for deploying Lambda functions. + +Globals: + Function: + MemorySize: 256 + Runtime: provided + CodeUri: ../../.build/lambda/LambdaAPI/lambda.zip + Environment: + Variables: + PRODUCTS_TABLE_NAME: product-table-sam + +Resources: +# APIGateway Function + createProduct: + Type: AWS::Serverless::Function + Properties: + Handler: create + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ProductTable + Events: + createProduct: + Type: HttpApi + Properties: + ApiId: !Ref lambdaApiGateway + Path: '/products' + Method: POST + readProduct: + Type: AWS::Serverless::Function + Properties: + Handler: read + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ProductTable + Events: + readProduct: + Type: HttpApi + Properties: + ApiId: !Ref lambdaApiGateway + Path: '/products/{sku}' + Method: GET + updateProduct: + Type: AWS::Serverless::Function + Properties: + Handler: update + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ProductTable + Events: + updateProduct: + Type: HttpApi + Properties: + ApiId: !Ref lambdaApiGateway + Path: '/products' + Method: PUT + deleteProduct: + Type: AWS::Serverless::Function + Properties: + Handler: delete + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ProductTable + Events: + deleteProduct: + Type: HttpApi + Properties: + ApiId: !Ref lambdaApiGateway + Path: '/products/{sku}' + Method: DELETE + listProducts: + Type: AWS::Serverless::Function + Properties: + Handler: list + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ProductTable + Events: + listProducts: + Type: HttpApi + Properties: + ApiId: !Ref lambdaApiGateway + Path: '/products' + Method: GET + + lambdaApiGateway: + Type: AWS::Serverless::HttpApi + + ProductTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: product-table-sam + AttributeDefinitions: + - AttributeName: sku + AttributeType: S + KeySchema: + - AttributeName: sku + KeyType: HASH + BillingMode: PAY_PER_REQUEST + +Outputs: + LambdaApiGatewayEndpoint: + Description: 'API Gateway endpoint URL.' + Value: !Sub 'https://${lambdaApiGateway}.execute-api.${AWS::Region}.amazonaws.com' diff --git a/Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml b/Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml new file mode 100644 index 00000000..5a0045bd --- /dev/null +++ b/Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml @@ -0,0 +1,96 @@ +service: swift-awslambda-runtime-api + +# package: +# individually: true +package: + artifact: .build/lambda/LambdaAPI/lambda.zip + +provider: + name: aws + httpApi: + payload: '2.0' + runtime: provided + environment: + PRODUCTS_TABLE_NAME: "${self:custom.productsTableName}" + iamRoleStatements: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + - Effect: Allow + Action: + - dynamodb:UpdateItem + - dynamodb:PutItem + - dynamodb:GetItem + - dynamodb:DeleteItem + - dynamodb:Query + - dynamodb:Scan + - dynamodb:DescribeTable + Resource: + - { Fn::GetAtt: [ProductsTable, Arn] } + +custom: + productsTableName: products-table-${self:provider.stage} + +functions: + createProduct: + handler: create + memorySize: 256 + description: "[${self:provider.stage}] Create Product" + events: + - httpApi: + path: /products + method: post + cors: true + readProduct: + handler: read + memorySize: 256 + description: "[${self:provider.stage}] Get Product" + events: + - httpApi: + path: /products/{sku} + method: get + cors: true + updateProduct: + handler: update + memorySize: 256 + description: "[${self:provider.stage}] Update Product" + events: + - httpApi: + path: /products + method: put + cors: true + deleteProduct: + handler: delete + memorySize: 256 + description: "[${self:provider.stage}] Delete Product" + events: + - httpApi: + path: /products/{sku} + method: delete + cors: true + listProducts: + handler: list + memorySize: 256 + description: "[${self:provider.stage}] List Products" + events: + - httpApi: + path: /products + method: get + cors: true + +resources: + Resources: + ProductsTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: ${self:custom.productsTableName} + AttributeDefinitions: + - AttributeName: sku + AttributeType: S + KeySchema: + - AttributeName: sku + KeyType: HASH + BillingMode: PAY_PER_REQUEST From e8436ff62ee68e8a5b8d730f701201da2eef91c6 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 08:09:39 +0200 Subject: [PATCH 02/10] Rename LambdaAPI to Product API Fix ProductLambda init --- Examples/LambdaFunctions/Package.swift | 4 ++-- .../APIGateway+Extensions.swift | 0 .../Product+DynamoDB.swift | 0 .../{LambdaAPI => ProductAPI}/Product.swift | 0 .../ProductLambda.swift | 18 ++++++++++++------ .../ProductLambdaHandler.swift | 0 .../ProductService.swift | 0 .../{LambdaAPI => ProductAPI}/main.swift | 0 .../{LambdaAPI => ProductAPI}/swagger.json | 0 ...PI-template.yml => ProductAPI-template.yml} | 2 +- ...PI-template.yml => ProductAPI-template.yml} | 2 +- 11 files changed, 16 insertions(+), 10 deletions(-) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/APIGateway+Extensions.swift (100%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/Product+DynamoDB.swift (100%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/Product.swift (100%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/ProductLambda.swift (86%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/ProductLambdaHandler.swift (100%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/ProductService.swift (100%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/main.swift (100%) rename Examples/LambdaFunctions/Sources/{LambdaAPI => ProductAPI}/swagger.json (100%) rename Examples/LambdaFunctions/scripts/SAM/{LambdaAPI-template.yml => ProductAPI-template.yml} (97%) rename Examples/LambdaFunctions/scripts/serverless/{LambdaAPI-template.yml => ProductAPI-template.yml} (98%) diff --git a/Examples/LambdaFunctions/Package.swift b/Examples/LambdaFunctions/Package.swift index e3220a35..d849ce4e 100644 --- a/Examples/LambdaFunctions/Package.swift +++ b/Examples/LambdaFunctions/Package.swift @@ -19,7 +19,7 @@ let package = Package( // fully featured example with domain specific business logic .executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]), // Full REST API Example using APIGateway, Lambda, DynamoDB - .executable(name: "LambdaAPI", targets: ["LambdaAPI"]), + .executable(name: "ProductAPI", targets: ["ProductAPI"]), ], dependencies: [ // this is the dependency on the swift-aws-lambda-runtime library @@ -50,7 +50,7 @@ let package = Package( .target(name: "CurrencyExchange", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), ]), - .target(name: "LambdaAPI", dependencies: [ + .target(name: "ProductAPI", dependencies: [ .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), .product(name: "AWSDynamoDB", package: "aws-sdk-swift"), diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/APIGateway+Extensions.swift b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/APIGateway+Extensions.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/Product+DynamoDB.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/Product+DynamoDB.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/Product.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/Product.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/Product.swift diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift similarity index 86% rename from Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift index 6eb41355..f2ee7348 100644 --- a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambda.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift @@ -65,13 +65,13 @@ struct ProductLambda: LambdaHandler { } static func tableName() throws -> String { - guard let tableName = ProcessInfo.processInfo.environment["PRODUCTS_TABLE_NAME"] else { + guard let tableName = Lambda.env("PRODUCTS_TABLE_NAME") else { throw APIError.tableNameNotFound } return tableName } - init(eventLoop: EventLoop) { + init(context: Lambda.InitializationContext) { let handler = Lambda.env("_HANDLER") ?? "" self.operation = Operation(rawValue: handler) ?? .unknown @@ -80,11 +80,17 @@ struct ProductLambda: LambdaHandler { logger.info("\(Self.currentRegion())") let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout) - let timeout = HTTPClient.Configuration.Timeout(connect: lambdaRuntimeTimeout, - read: lambdaRuntimeTimeout) + let timeout = HTTPClient.Configuration.Timeout( + connect: lambdaRuntimeTimeout, + read: lambdaRuntimeTimeout + ) + let configuration = HTTPClient.Configuration(timeout: timeout) - self.httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: configuration) - + self.httpClient = HTTPClient( + eventLoopGroupProvider: .shared(context.eventLoop), + configuration: configuration + ) + self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient)) logger.info("DynamoDB") diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/ProductLambdaHandler.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/ProductService.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/main.swift b/Examples/LambdaFunctions/Sources/ProductAPI/main.swift similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/main.swift rename to Examples/LambdaFunctions/Sources/ProductAPI/main.swift diff --git a/Examples/LambdaFunctions/Sources/LambdaAPI/swagger.json b/Examples/LambdaFunctions/Sources/ProductAPI/swagger.json similarity index 100% rename from Examples/LambdaFunctions/Sources/LambdaAPI/swagger.json rename to Examples/LambdaFunctions/Sources/ProductAPI/swagger.json diff --git a/Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml b/Examples/LambdaFunctions/scripts/SAM/ProductAPI-template.yml similarity index 97% rename from Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml rename to Examples/LambdaFunctions/scripts/SAM/ProductAPI-template.yml index 6575dd03..54610180 100644 --- a/Examples/LambdaFunctions/scripts/SAM/LambdaAPI-template.yml +++ b/Examples/LambdaFunctions/scripts/SAM/ProductAPI-template.yml @@ -6,7 +6,7 @@ Globals: Function: MemorySize: 256 Runtime: provided - CodeUri: ../../.build/lambda/LambdaAPI/lambda.zip + CodeUri: ../../.build/lambda/ProductAPI/lambda.zip Environment: Variables: PRODUCTS_TABLE_NAME: product-table-sam diff --git a/Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml b/Examples/LambdaFunctions/scripts/serverless/ProductAPI-template.yml similarity index 98% rename from Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml rename to Examples/LambdaFunctions/scripts/serverless/ProductAPI-template.yml index 5a0045bd..1198e8a5 100644 --- a/Examples/LambdaFunctions/scripts/serverless/LambdaAPI-template.yml +++ b/Examples/LambdaFunctions/scripts/serverless/ProductAPI-template.yml @@ -3,7 +3,7 @@ service: swift-awslambda-runtime-api # package: # individually: true package: - artifact: .build/lambda/LambdaAPI/lambda.zip + artifact: .build/lambda/ProductAPI/lambda.zip provider: name: aws From 563f83525294995e89cc7029ff8d24b07d0ecfa1 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 09:40:06 +0200 Subject: [PATCH 03/10] Improve formatting and remove unused import --- .../ProductAPI/APIGateway+Extensions.swift | 28 +- .../Sources/ProductAPI/Product+DynamoDB.swift | 22 +- .../Sources/ProductAPI/Product.swift | 3 - .../Sources/ProductAPI/ProductLambda.swift | 31 +- .../ProductAPI/ProductLambdaHandler.swift | 290 +++++++++--------- .../Sources/ProductAPI/ProductService.swift | 10 +- 6 files changed, 198 insertions(+), 186 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift index b7040eed..baaa1a81 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift @@ -12,34 +12,37 @@ // //===----------------------------------------------------------------------===// -import Foundation import AWSLambdaEvents +import Foundation -public extension APIGateway.V2.Request { - func object() throws -> T { +extension APIGateway.V2.Request { + public func object() throws -> T { let decoder = JSONDecoder() guard let body = self.body, - let dataBody = body.data(using: .utf8) else { + let dataBody = body.data(using: .utf8) + else { throw APIError.invalidRequest } return try decoder.decode(T.self, from: dataBody) } } -let defaultHeaders = ["Content-Type": "application/json", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE", - "Access-Control-Allow-Credentials": "true"] - extension APIGateway.V2.Response { + static let defaultHeaders = [ + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE", + "Access-Control-Allow-Credentials": "true", + ] + init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { self.init( statusCode: statusCode, - headers: defaultHeaders, + headers: APIGateway.V2.Response.defaultHeaders, multiValueHeaders: nil, - body: "{\"message\":\"\(error.localizedDescription)\"}", + body: "{\"message\":\"\(String(describing: error))\"}", isBase64Encoded: false ) } @@ -53,7 +56,7 @@ extension APIGateway.V2.Response { } self.init( statusCode: statusCode, - headers: defaultHeaders, + headers: APIGateway.V2.Response.defaultHeaders, multiValueHeaders: nil, body: body, isBase64Encoded: false @@ -62,7 +65,6 @@ extension APIGateway.V2.Response { } init(with result: Result, statusCode: AWSLambdaEvents.HTTPResponseStatus) { - switch result { case .success(let value): self.init(with: value, statusCode: statusCode) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift index bd3bd7ea..d9a24a70 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import Foundation import AWSDynamoDB public struct ProductField { @@ -23,25 +22,28 @@ public struct ProductField { static let updatedAt = "updatedAt" } -public extension Product { - var dynamoDictionary: [String : DynamoDB.AttributeValue] { - var dictionary = [ProductField.sku: DynamoDB.AttributeValue(s:sku), - ProductField.name: DynamoDB.AttributeValue(s:name), - ProductField.description: DynamoDB.AttributeValue(s:description)] +extension Product { + public var dynamoDictionary: [String: DynamoDB.AttributeValue] { + var dictionary = [ + ProductField.sku: DynamoDB.AttributeValue(s: sku), + ProductField.name: DynamoDB.AttributeValue(s: name), + ProductField.description: DynamoDB.AttributeValue(s: description), + ] if let createdAt = createdAt { - dictionary[ProductField.createdAt] = DynamoDB.AttributeValue(s:createdAt) + dictionary[ProductField.createdAt] = DynamoDB.AttributeValue(s: createdAt) } if let updatedAt = updatedAt { - dictionary[ProductField.updatedAt] = DynamoDB.AttributeValue(s:updatedAt) + dictionary[ProductField.updatedAt] = DynamoDB.AttributeValue(s: updatedAt) } return dictionary } - init(dictionary: [String: DynamoDB.AttributeValue]) throws { + public init(dictionary: [String: DynamoDB.AttributeValue]) throws { guard let name = dictionary[ProductField.name]?.s, let sku = dictionary[ProductField.sku]?.s, - let description = dictionary[ProductField.description]?.s else { + let description = dictionary[ProductField.description]?.s + else { throw APIError.invalidItem } self.name = name diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift index 4ec5b1d4..78fe2694 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift @@ -12,8 +12,6 @@ // //===----------------------------------------------------------------------===// -import Foundation - public struct Product: Codable { public let sku: String public let name: String @@ -21,4 +19,3 @@ public struct Product: Codable { public var createdAt: String? public var updatedAt: String? } - diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift index f2ee7348..49eba528 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift @@ -12,16 +12,12 @@ // //===----------------------------------------------------------------------===// -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif -import AWSLambdaRuntime import AWSDynamoDB -import NIO -import Logging -import AsyncHTTPClient import AWSLambdaEvents +import AWSLambdaRuntime +import AsyncHTTPClient +import Logging +import NIO enum Operation: String { case create @@ -42,19 +38,19 @@ struct ProductLambda: LambdaHandler { typealias In = APIGateway.V2.Request typealias Out = APIGateway.V2.Response - let dbTimeout:Int64 = 30 + let dbTimeout: Int64 = 30 let region: Region let db: AWSDynamoDB.DynamoDB let service: ProductService let tableName: String let operation: Operation - + var httpClient: HTTPClient static func currentRegion() -> Region { - if let awsRegion = ProcessInfo.processInfo.environment["AWS_REGION"] { + if let awsRegion = Lambda.env("AWS_REGION") { let value = Region(rawValue: awsRegion) return value @@ -78,7 +74,7 @@ struct ProductLambda: LambdaHandler { self.region = Self.currentRegion() logger.info("\(Self.currentRegion())") - + let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout) let timeout = HTTPClient.Configuration.Timeout( connect: lambdaRuntimeTimeout, @@ -102,9 +98,14 @@ struct ProductLambda: LambdaHandler { ) logger.info("ProductService") } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request, callback: @escaping (Result) -> Void) { - let _ = ProductLambdaHandler(service: service, operation: operation).handle(context: context, event: event) + + func handle( + context: Lambda.Context, event: APIGateway.V2.Request, + callback: @escaping (Result) -> Void + ) { + let _ = ProductLambdaHandler(service: service, operation: operation).handle( + context: context, event: event + ) .always { (result) in callback(result) } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift index 82e2a4f2..af7b254e 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -12,14 +12,10 @@ // //===----------------------------------------------------------------------===// -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif +import AWSLambdaEvents import AWSLambdaRuntime -import NIO import Logging -import AWSLambdaEvents +import NIO struct ProductLambdaHandler: EventLoopLambdaHandler { @@ -29,84 +25,88 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { let service: ProductService let operation: Operation - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { - - switch operation { - case .create: - logger.info("create") - let create = CreateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .created) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) - return context.eventLoop.makeSucceededFuture(value) + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< + APIGateway.V2.Response + > { + + switch operation { + case .create: + logger.info("create") + let create = CreateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .created) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) + } } - } - return create - case .read: - logger.info("read") - let read = ReadLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .notFound) - return context.eventLoop.makeSucceededFuture(value) + return create + case .read: + logger.info("read") + let read = ReadLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) + } } - } - return read - case .update: - logger.info("update") - let update = UpdateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .notFound) - return context.eventLoop.makeSucceededFuture(value) + return read + case .update: + logger.info("update") + let update = UpdateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) + } } - } - return update - case .delete: - logger.info("delete") - let delete = DeleteUpdateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .notFound) - return context.eventLoop.makeSucceededFuture(value) + return update + case .delete: + logger.info("delete") + let delete = DeleteUpdateLambdaHandler(service: service).handle( + context: context, event: event + ) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) + } } - } - return delete - case .list: - logger.info("list") - let list = ListUpdateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) - return context.eventLoop.makeSucceededFuture(value) + return delete + case .list: + logger.info("list") + let list = ListUpdateLambdaHandler(service: service).handle(context: context, event: event) + .flatMap { response -> EventLoopFuture in + switch response { + case .success(let result): + let value = APIGateway.V2.Response(with: result, statusCode: .ok) + return context.eventLoop.makeSucceededFuture(value) + case .failure(let error): + let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) + } } + return list + case .unknown: + logger.info("unknown") + let value = APIGateway.V2.Response(with: APIError.handlerNotFound, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) } - return list - case .unknown: - logger.info("unknown") - let value = APIGateway.V2.Response(with: APIError.handlerNotFound, statusCode: .forbidden) - return context.eventLoop.makeSucceededFuture(value) - } } struct CreateLambdaHandler { @@ -116,41 +116,45 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { init(service: ProductService) { self.service = service } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let product: Product = try? event.object() else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.createItem(product: product) - .flatMapThrowing { item -> Result in - return Result.success(product) - } - return future + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< + Result + > { + + guard let product: Product = try? event.object() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.createItem(product: product) + .flatMapThrowing { item -> Result in + return Result.success(product) + } + return future } } struct ReadLambdaHandler { - + let service: ProductService init(service: ProductService) { self.service = service } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let sku = event.pathParameters?["sku"] else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.readItem(key: sku) - .flatMapThrowing { data -> Result in - let product = try Product(dictionary: data.item ?? [:]) - return Result.success(product) - } - return future + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< + Result + > { + + guard let sku = event.pathParameters?["sku"] else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.readItem(key: sku) + .flatMapThrowing { data -> Result in + let product = try Product(dictionary: data.item ?? [:]) + return Result.success(product) + } + return future } } @@ -161,18 +165,20 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { init(service: ProductService) { self.service = service } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let product: Product = try? event.object() else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.updateItem(product: product) - .flatMapThrowing { (data) -> Result in - return Result.success(product) - } - return future + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< + Result + > { + + guard let product: Product = try? event.object() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.updateItem(product: product) + .flatMapThrowing { (data) -> Result in + return Result.success(product) + } + return future } } @@ -183,18 +189,20 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { init(service: ProductService) { self.service = service } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let sku = event.pathParameters?["sku"] else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.deleteItem(key: sku) - .flatMapThrowing { (data) -> Result in - return Result.success(EmptyResponse()) - } - return future + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< + Result + > { + + guard let sku = event.pathParameters?["sku"] else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + let future = service.deleteItem(key: sku) + .flatMapThrowing { (data) -> Result in + return Result.success(EmptyResponse()) + } + return future } } @@ -205,18 +213,20 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { init(service: ProductService) { self.service = service } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - let future = service.listItems() - .flatMapThrowing { data -> Result<[Product],Error> in - let products: [Product]? = try data.items?.compactMap { (item) -> Product in - return try Product(dictionary: item) - } - let object = products ?? [] - return Result.success(object) - } - return future + + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< + Result<[Product], Error> + > { + + let future = service.listItems() + .flatMapThrowing { data -> Result<[Product], Error> in + let products: [Product]? = try data.items?.compactMap { (item) -> Product in + return try Product(dictionary: item) + } + let object = products ?? [] + return Result.success(object) + } + return future } } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift index 1e05a5a0..330fd591 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -import Foundation import AWSDynamoDB +import Foundation import NIO public enum APIError: Error { @@ -74,12 +74,12 @@ public class ProductService { expressionAttributeNames: [ "#name": ProductField.name, "#description": ProductField.description, - "#updatedAt": ProductField.updatedAt + "#updatedAt": ProductField.updatedAt, ], expressionAttributeValues: [ - ":name": DynamoDB.AttributeValue(s:product.name), - ":description": DynamoDB.AttributeValue(s:product.description), - ":updatedAt": DynamoDB.AttributeValue(s:product.updatedAt) + ":name": DynamoDB.AttributeValue(s: product.name), + ":description": DynamoDB.AttributeValue(s: product.description), + ":updatedAt": DynamoDB.AttributeValue(s: product.updatedAt), ], key: [ProductField.sku: DynamoDB.AttributeValue(s: product.sku)], returnValues: DynamoDB.ReturnValue.allNew, From 9f875dcd0cbe73edcd0ee0190fee5f94ec85cc5f Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 09:49:42 +0200 Subject: [PATCH 04/10] Improve formatting --- .../Sources/ProductAPI/APIGateway+Extensions.swift | 3 --- .../Sources/ProductAPI/ProductLambda.swift | 12 ++---------- .../Sources/ProductAPI/ProductLambdaHandler.swift | 12 +++--------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift index baaa1a81..ee6679c2 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift @@ -37,7 +37,6 @@ extension APIGateway.V2.Response { ] init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { - self.init( statusCode: statusCode, headers: APIGateway.V2.Response.defaultHeaders, @@ -49,7 +48,6 @@ extension APIGateway.V2.Response { init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) { let encoder = JSONEncoder() - var body: String = "{}" if let data = try? encoder.encode(object) { body = String(data: data, encoding: .utf8) ?? body @@ -61,7 +59,6 @@ extension APIGateway.V2.Response { body: body, isBase64Encoded: false ) - } init(with result: Result, statusCode: AWSLambdaEvents.HTTPResponseStatus) { diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift index 49eba528..fa7640d2 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift @@ -34,28 +34,22 @@ struct EmptyResponse: Codable { struct ProductLambda: LambdaHandler { - //typealias In = APIGateway.SimpleRequest typealias In = APIGateway.V2.Request typealias Out = APIGateway.V2.Response let dbTimeout: Int64 = 30 - let region: Region let db: AWSDynamoDB.DynamoDB let service: ProductService let tableName: String let operation: Operation - var httpClient: HTTPClient static func currentRegion() -> Region { - if let awsRegion = Lambda.env("AWS_REGION") { let value = Region(rawValue: awsRegion) return value - } else { - //Default configuration return .useast1 } } @@ -89,7 +83,6 @@ struct ProductLambda: LambdaHandler { self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient)) logger.info("DynamoDB") - self.tableName = (try? Self.tableName()) ?? "" self.service = ProductService( @@ -103,9 +96,8 @@ struct ProductLambda: LambdaHandler { context: Lambda.Context, event: APIGateway.V2.Request, callback: @escaping (Result) -> Void ) { - let _ = ProductLambdaHandler(service: service, operation: operation).handle( - context: context, event: event - ) + let _ = ProductLambdaHandler(service: service, operation: operation) + .handle(context: context, event: event) .always { (result) in callback(result) } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift index af7b254e..ddfe9607 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -166,9 +166,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { self.service = service } - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< - Result - > { + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { guard let product: Product = try? event.object() else { let error = APIError.invalidRequest @@ -190,9 +188,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { self.service = service } - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< - Result - > { + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { guard let sku = event.pathParameters?["sku"] else { let error = APIError.invalidRequest @@ -214,9 +210,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { self.service = service } - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< - Result<[Product], Error> - > { + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { let future = service.listItems() .flatMapThrowing { data -> Result<[Product], Error> in From 4b56aa5563698461816f6d2d4943a431cd064ff7 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 10:20:06 +0200 Subject: [PATCH 05/10] Refactor Operation --- .../Sources/ProductAPI/ProductLambda.swift | 24 ++++++++----------- .../ProductAPI/ProductLambdaHandler.swift | 4 ---- .../Sources/ProductAPI/ProductService.swift | 2 +- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift index fa7640d2..de1402a9 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift @@ -25,12 +25,9 @@ enum Operation: String { case update case delete case list - case unknown } -struct EmptyResponse: Codable { - -} +struct EmptyResponse: Codable {} struct ProductLambda: LambdaHandler { @@ -61,20 +58,21 @@ struct ProductLambda: LambdaHandler { return tableName } - init(context: Lambda.InitializationContext) { - - let handler = Lambda.env("_HANDLER") ?? "" - self.operation = Operation(rawValue: handler) ?? .unknown + init(context: Lambda.InitializationContext) throws { + guard let handler = Lambda.env("_HANDLER"), + let operation = Operation(rawValue: handler) else { + throw APIError.invalidHandler + } + self.operation = operation self.region = Self.currentRegion() - logger.info("\(Self.currentRegion())") let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout) let timeout = HTTPClient.Configuration.Timeout( connect: lambdaRuntimeTimeout, read: lambdaRuntimeTimeout ) - + let configuration = HTTPClient.Configuration(timeout: timeout) self.httpClient = HTTPClient( eventLoopGroupProvider: .shared(context.eventLoop), @@ -82,14 +80,12 @@ struct ProductLambda: LambdaHandler { ) self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient)) - logger.info("DynamoDB") - self.tableName = (try? Self.tableName()) ?? "" - + self.tableName = try Self.tableName() + self.service = ProductService( db: db, tableName: tableName ) - logger.info("ProductService") } func handle( diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift index ddfe9607..941158fd 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -102,10 +102,6 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } } return list - case .unknown: - logger.info("unknown") - let value = APIGateway.V2.Response(with: APIError.handlerNotFound, statusCode: .forbidden) - return context.eventLoop.makeSucceededFuture(value) } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift index 330fd591..485b4acd 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift @@ -20,7 +20,7 @@ public enum APIError: Error { case invalidItem case tableNameNotFound case invalidRequest - case handlerNotFound + case invalidHandler } extension Date { From 381aecd351ef82025cbfcc845d551114b2f09ceb Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 10:24:52 +0200 Subject: [PATCH 06/10] Remove Logging --- .../ProductAPI/ProductLambdaHandler.swift | 17 +++-------------- .../Sources/ProductAPI/main.swift | 3 --- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift index 941158fd..ad5a1569 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -25,13 +25,10 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { let service: ProductService let operation: Operation - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< - APIGateway.V2.Response - > { + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { switch operation { case .create: - logger.info("create") let create = CreateLambdaHandler(service: service).handle(context: context, event: event) .flatMap { response -> EventLoopFuture in switch response { @@ -45,7 +42,6 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } return create case .read: - logger.info("read") let read = ReadLambdaHandler(service: service).handle(context: context, event: event) .flatMap { response -> EventLoopFuture in switch response { @@ -59,7 +55,6 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } return read case .update: - logger.info("update") let update = UpdateLambdaHandler(service: service).handle(context: context, event: event) .flatMap { response -> EventLoopFuture in switch response { @@ -73,7 +68,6 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } return update case .delete: - logger.info("delete") let delete = DeleteUpdateLambdaHandler(service: service).handle( context: context, event: event ) @@ -89,7 +83,6 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } return delete case .list: - logger.info("list") let list = ListUpdateLambdaHandler(service: service).handle(context: context, event: event) .flatMap { response -> EventLoopFuture in switch response { @@ -113,9 +106,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { self.service = service } - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< - Result - > { + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { guard let product: Product = try? event.object() else { let error = APIError.invalidRequest @@ -137,9 +128,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { self.service = service } - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture< - Result - > { + func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { guard let sku = event.pathParameters?["sku"] else { let error = APIError.invalidRequest diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/main.swift b/Examples/LambdaFunctions/Sources/ProductAPI/main.swift index 369a5397..b5ff1fa9 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/main.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/main.swift @@ -13,8 +13,5 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime -import Logging - -let logger = Logger(label: "AWS.Lambda.Products") Lambda.run(ProductLambda.init) From c55838adcff8bd1b758a0cc3ed565dfb76b265b1 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 11:58:34 +0200 Subject: [PATCH 07/10] Refactor ProductService and ProductLambdaHandler --- .../ProductAPI/ProductLambdaHandler.swift | 222 +++++------------- .../Sources/ProductAPI/ProductService.swift | 31 ++- 2 files changed, 81 insertions(+), 172 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift index ad5a1569..715ebd06 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -26,186 +26,84 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { let operation: Operation func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { - - switch operation { - case .create: - let create = CreateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .created) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) - return context.eventLoop.makeSucceededFuture(value) - } - } - return create - case .read: - let read = ReadLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .notFound) - return context.eventLoop.makeSucceededFuture(value) - } - } - return read - case .update: - let update = UpdateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .notFound) - return context.eventLoop.makeSucceededFuture(value) - } - } - return update - case .delete: - let delete = DeleteUpdateLambdaHandler(service: service).handle( - context: context, event: event - ) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .notFound) - return context.eventLoop.makeSucceededFuture(value) - } - } - return delete - case .list: - let list = ListUpdateLambdaHandler(service: service).handle(context: context, event: event) - .flatMap { response -> EventLoopFuture in - switch response { - case .success(let result): - let value = APIGateway.V2.Response(with: result, statusCode: .ok) - return context.eventLoop.makeSucceededFuture(value) - case .failure(let error): - let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) - return context.eventLoop.makeSucceededFuture(value) - } - } - return list - } + + switch self.operation { + case .create: + return createLambdaHandler(context: context, event: event) + case .read: + return readLambdaHandler(context: context, event: event) + case .update: + return updateLambdaHandler(context: context, event: event) + case .delete: + return deleteUpdateLambdaHandler(context: context, event: event) + case .list: + return listUpdateLambdaHandler(context: context, event: event) + } } - struct CreateLambdaHandler { - - let service: ProductService - - init(service: ProductService) { - self.service = service + func createLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + guard let product: Product = try? event.object() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let product: Product = try? event.object() else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.createItem(product: product) - .flatMapThrowing { item -> Result in - return Result.success(product) - } - return future + return service.createItem(product: product) + .map { result -> (APIGateway.V2.Response) in + return APIGateway.V2.Response(with: result, statusCode: .created) + }.flatMapError { (error) -> EventLoopFuture in + let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) } } - struct ReadLambdaHandler { - - let service: ProductService - - init(service: ProductService) { - self.service = service + func readLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + guard let sku = event.pathParameters?["sku"] else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let sku = event.pathParameters?["sku"] else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.readItem(key: sku) - .flatMapThrowing { data -> Result in - let product = try Product(dictionary: data.item ?? [:]) - return Result.success(product) - } - return future + return service.readItem(key: sku) + .flatMapThrowing { result -> APIGateway.V2.Response in + return APIGateway.V2.Response(with: result, statusCode: .ok) + }.flatMapError { (error) -> EventLoopFuture in + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) } } - struct UpdateLambdaHandler { - - let service: ProductService - - init(service: ProductService) { - self.service = service + func updateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + guard let product: Product = try? event.object() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let product: Product = try? event.object() else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.updateItem(product: product) - .flatMapThrowing { (data) -> Result in - return Result.success(product) - } - return future + return service.updateItem(product: product) + .map { result -> (APIGateway.V2.Response) in + return APIGateway.V2.Response(with: result, statusCode: .ok) + }.flatMapError { (error) -> EventLoopFuture in + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) } } - struct DeleteUpdateLambdaHandler { - - let service: ProductService - - init(service: ProductService) { - self.service = service + func deleteUpdateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + guard let sku = event.pathParameters?["sku"] else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - guard let sku = event.pathParameters?["sku"] else { - let error = APIError.invalidRequest - return context.eventLoop.makeFailedFuture(error) - } - let future = service.deleteItem(key: sku) - .flatMapThrowing { (data) -> Result in - return Result.success(EmptyResponse()) - } - return future + return service.deleteItem(key: sku) + .map { _ -> (APIGateway.V2.Response) in + return APIGateway.V2.Response(with: EmptyResponse(), statusCode: .ok) + }.flatMapError { (error) -> EventLoopFuture in + let value = APIGateway.V2.Response(with: error, statusCode: .notFound) + return context.eventLoop.makeSucceededFuture(value) } } - struct ListUpdateLambdaHandler { - - let service: ProductService - - init(service: ProductService) { - self.service = service - } - - func handle(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture> { - - let future = service.listItems() - .flatMapThrowing { data -> Result<[Product], Error> in - let products: [Product]? = try data.items?.compactMap { (item) -> Product in - return try Product(dictionary: item) - } - let object = products ?? [] - return Result.success(object) - } - return future + func listUpdateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + return service.listItems() + .flatMapThrowing { result -> APIGateway.V2.Response in + return APIGateway.V2.Response(with: result, statusCode: .ok) + }.flatMapError { (error) -> EventLoopFuture in + let value = APIGateway.V2.Response(with: error, statusCode: .forbidden) + return context.eventLoop.makeSucceededFuture(value) } } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift index 485b4acd..2517b4fe 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift @@ -42,7 +42,7 @@ public class ProductService { self.tableName = tableName } - public func createItem(product: Product) -> EventLoopFuture { + public func createItem(product: Product) -> EventLoopFuture { var product = product let date = Date().iso8601 @@ -53,19 +53,22 @@ public class ProductService { item: product.dynamoDictionary, tableName: tableName ) - return db.putItem(input) + return db.putItem(input).flatMap { _ -> EventLoopFuture in + return self.readItem(key: product.sku) + } } - public func readItem(key: String) -> EventLoopFuture { + public func readItem(key: String) -> EventLoopFuture { let input = DynamoDB.GetItemInput( key: [ProductField.sku: DynamoDB.AttributeValue(s: key)], tableName: tableName ) - return db.getItem(input) + return db.getItem(input).flatMapThrowing { data -> Product in + return try Product(dictionary: data.item ?? [:]) + } } - public func updateItem(product: Product) -> EventLoopFuture { - + public func updateItem(product: Product) -> EventLoopFuture { var product = product let date = Date().iso8601 product.updatedAt = date @@ -86,19 +89,27 @@ public class ProductService { tableName: tableName, updateExpression: "SET #name = :name, #description = :description, #updatedAt = :updatedAt" ) - return db.updateItem(input) + return db.updateItem(input).flatMap { _ -> EventLoopFuture in + return self.readItem(key: product.sku) + } } - public func deleteItem(key: String) -> EventLoopFuture { + public func deleteItem(key: String) -> EventLoopFuture { let input = DynamoDB.DeleteItemInput( key: [ProductField.sku: DynamoDB.AttributeValue(s: key)], tableName: tableName ) - return db.deleteItem(input) + return db.deleteItem(input).map { _ in Void() } } - public func listItems() -> EventLoopFuture { + public func listItems() -> EventLoopFuture<[Product]> { let input = DynamoDB.ScanInput(tableName: tableName) return db.scan(input) + .flatMapThrowing { data -> [Product] in + let products: [Product]? = try data.items?.compactMap { (item) -> Product in + return try Product(dictionary: item) + } + return products ?? [] + } } } From 2e1f458fa3bc25a6f597062fbaba12e91e0ebc05 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 12:46:24 +0200 Subject: [PATCH 08/10] Refactor ProductField --- .../Sources/ProductAPI/Product+DynamoDB.swift | 29 +++++++--------- .../Sources/ProductAPI/Product.swift | 10 ++++++ .../Sources/ProductAPI/ProductService.swift | 33 +++++++++++-------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift index d9a24a70..c3cc91f5 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift @@ -14,42 +14,35 @@ import AWSDynamoDB -public struct ProductField { - static let sku = "sku" - static let name = "name" - static let description = "description" - static let createdAt = "createdAt" - static let updatedAt = "updatedAt" -} - extension Product { + public var dynamoDictionary: [String: DynamoDB.AttributeValue] { var dictionary = [ - ProductField.sku: DynamoDB.AttributeValue(s: sku), - ProductField.name: DynamoDB.AttributeValue(s: name), - ProductField.description: DynamoDB.AttributeValue(s: description), + Field.sku: DynamoDB.AttributeValue(s: sku), + Field.name: DynamoDB.AttributeValue(s: name), + Field.description: DynamoDB.AttributeValue(s: description), ] if let createdAt = createdAt { - dictionary[ProductField.createdAt] = DynamoDB.AttributeValue(s: createdAt) + dictionary[Field.createdAt] = DynamoDB.AttributeValue(s: createdAt) } if let updatedAt = updatedAt { - dictionary[ProductField.updatedAt] = DynamoDB.AttributeValue(s: updatedAt) + dictionary[Field.updatedAt] = DynamoDB.AttributeValue(s: updatedAt) } return dictionary } public init(dictionary: [String: DynamoDB.AttributeValue]) throws { - guard let name = dictionary[ProductField.name]?.s, - let sku = dictionary[ProductField.sku]?.s, - let description = dictionary[ProductField.description]?.s + guard let name = dictionary[Field.name]?.s, + let sku = dictionary[Field.sku]?.s, + let description = dictionary[Field.description]?.s else { throw APIError.invalidItem } self.name = name self.sku = sku self.description = description - self.createdAt = dictionary[ProductField.createdAt]?.s - self.updatedAt = dictionary[ProductField.updatedAt]?.s + self.createdAt = dictionary[Field.createdAt]?.s + self.updatedAt = dictionary[Field.updatedAt]?.s } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift index 78fe2694..277c0bb2 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift @@ -12,10 +12,20 @@ // //===----------------------------------------------------------------------===// +import Foundation + public struct Product: Codable { public let sku: String public let name: String public let description: String public var createdAt: String? public var updatedAt: String? + + public struct Field { + static let sku = "sku" + static let name = "name" + static let description = "description" + static let createdAt = "createdAt" + static let updatedAt = "updatedAt" + } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift index 2517b4fe..cfbfcf5f 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift @@ -23,11 +23,18 @@ public enum APIError: Error { case invalidHandler } -extension Date { - var iso8601: String { +extension DateFormatter { + static var iso8061: DateFormatter { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter + } +} + +extension Date { + var iso8601: String { + let formatter = DateFormatter.iso8061 return formatter.string(from: self) } } @@ -45,9 +52,9 @@ public class ProductService { public func createItem(product: Product) -> EventLoopFuture { var product = product - let date = Date().iso8601 - product.createdAt = date - product.updatedAt = date + let date = Date() + product.createdAt = date.iso8601 + product.updatedAt = date.iso8601 let input = DynamoDB.PutItemInput( item: product.dynamoDictionary, @@ -60,7 +67,7 @@ public class ProductService { public func readItem(key: String) -> EventLoopFuture { let input = DynamoDB.GetItemInput( - key: [ProductField.sku: DynamoDB.AttributeValue(s: key)], + key: [Product.Field.sku: DynamoDB.AttributeValue(s: key)], tableName: tableName ) return db.getItem(input).flatMapThrowing { data -> Product in @@ -70,21 +77,21 @@ public class ProductService { public func updateItem(product: Product) -> EventLoopFuture { var product = product - let date = Date().iso8601 - product.updatedAt = date + let date = Date() + product.updatedAt = date.iso8601 let input = DynamoDB.UpdateItemInput( expressionAttributeNames: [ - "#name": ProductField.name, - "#description": ProductField.description, - "#updatedAt": ProductField.updatedAt, + "#name": Product.Field.name, + "#description": Product.Field.description, + "#updatedAt": Product.Field.updatedAt, ], expressionAttributeValues: [ ":name": DynamoDB.AttributeValue(s: product.name), ":description": DynamoDB.AttributeValue(s: product.description), ":updatedAt": DynamoDB.AttributeValue(s: product.updatedAt), ], - key: [ProductField.sku: DynamoDB.AttributeValue(s: product.sku)], + key: [Product.Field.sku: DynamoDB.AttributeValue(s: product.sku)], returnValues: DynamoDB.ReturnValue.allNew, tableName: tableName, updateExpression: "SET #name = :name, #description = :description, #updatedAt = :updatedAt" @@ -96,7 +103,7 @@ public class ProductService { public func deleteItem(key: String) -> EventLoopFuture { let input = DynamoDB.DeleteItemInput( - key: [ProductField.sku: DynamoDB.AttributeValue(s: key)], + key: [Product.Field.sku: DynamoDB.AttributeValue(s: key)], tableName: tableName ) return db.deleteItem(input).map { _ in Void() } From 86a89d8e56fa44e8336139061e9579b88119c6a6 Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 14:52:33 +0200 Subject: [PATCH 09/10] Refactor APIGateway+Extension --- .../ProductAPI/APIGateway+Extensions.swift | 32 ++++++++----------- .../ProductAPI/ProductLambdaHandler.swift | 4 +-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift index ee6679c2..2a278a05 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift @@ -13,30 +13,36 @@ //===----------------------------------------------------------------------===// import AWSLambdaEvents -import Foundation +import class Foundation.JSONEncoder +import class Foundation.JSONDecoder extension APIGateway.V2.Request { - public func object() throws -> T { - let decoder = JSONDecoder() + + static private let decoder = JSONDecoder() + + public func bodyObject() throws -> T { guard let body = self.body, let dataBody = body.data(using: .utf8) else { throw APIError.invalidRequest } - return try decoder.decode(T.self, from: dataBody) + return try Self.decoder.decode(T.self, from: dataBody) } } extension APIGateway.V2.Response { - static let defaultHeaders = [ + private static let encoder = JSONEncoder() + + public static let defaultHeaders = [ "Content-Type": "application/json", + //Security warning: XSS are enabled "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE", "Access-Control-Allow-Credentials": "true", ] - init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { + public init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { self.init( statusCode: statusCode, headers: APIGateway.V2.Response.defaultHeaders, @@ -46,10 +52,9 @@ extension APIGateway.V2.Response { ) } - init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) { - let encoder = JSONEncoder() + public init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) { var body: String = "{}" - if let data = try? encoder.encode(object) { + if let data = try? Self.encoder.encode(object) { body = String(data: data, encoding: .utf8) ?? body } self.init( @@ -60,13 +65,4 @@ extension APIGateway.V2.Response { isBase64Encoded: false ) } - - init(with result: Result, statusCode: AWSLambdaEvents.HTTPResponseStatus) { - switch result { - case .success(let value): - self.init(with: value, statusCode: statusCode) - case .failure(let error): - self.init(with: error, statusCode: statusCode) - } - } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift index 715ebd06..18a33486 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -42,7 +42,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } func createLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { - guard let product: Product = try? event.object() else { + guard let product: Product = try? event.bodyObject() else { let error = APIError.invalidRequest return context.eventLoop.makeFailedFuture(error) } @@ -70,7 +70,7 @@ struct ProductLambdaHandler: EventLoopLambdaHandler { } func updateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { - guard let product: Product = try? event.object() else { + guard let product: Product = try? event.bodyObject() else { let error = APIError.invalidRequest return context.eventLoop.makeFailedFuture(error) } From 074b7a03e3faeb0fbe1fe62cea5995b396fe159c Mon Sep 17 00:00:00 2001 From: Andrea Scuderi Date: Sat, 20 Jun 2020 15:58:18 +0200 Subject: [PATCH 10/10] Store createdAt and updatedAt as TimeIntervalSince1970 in DynamoDB --- .../Sources/ProductAPI/Product+DynamoDB.swift | 21 +++++++++++++------ .../Sources/ProductAPI/ProductService.swift | 19 ++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift index c3cc91f5..542b8db1 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import AWSDynamoDB +import Foundation extension Product { @@ -22,12 +23,12 @@ extension Product { Field.name: DynamoDB.AttributeValue(s: name), Field.description: DynamoDB.AttributeValue(s: description), ] - if let createdAt = createdAt { - dictionary[Field.createdAt] = DynamoDB.AttributeValue(s: createdAt) + if let createdAt = createdAt?.timeIntervalSince1970String { + dictionary[Field.createdAt] = DynamoDB.AttributeValue(n: createdAt) } - if let updatedAt = updatedAt { - dictionary[Field.updatedAt] = DynamoDB.AttributeValue(s: updatedAt) + if let updatedAt = updatedAt?.timeIntervalSince1970String { + dictionary[Field.updatedAt] = DynamoDB.AttributeValue(n: updatedAt) } return dictionary } @@ -42,7 +43,15 @@ extension Product { self.name = name self.sku = sku self.description = description - self.createdAt = dictionary[Field.createdAt]?.s - self.updatedAt = dictionary[Field.updatedAt]?.s + if let value = dictionary[Field.createdAt]?.n, + let timeInterval = TimeInterval(value) { + let date = Date(timeIntervalSince1970: timeInterval) + self.createdAt = date.iso8601 + } + if let value = dictionary[Field.updatedAt]?.n, + let timeInterval = TimeInterval(value) { + let date = Date(timeIntervalSince1970: timeInterval) + self.updatedAt = date.iso8601 + } } } diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift index cfbfcf5f..d65bb992 100644 --- a/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift @@ -39,6 +39,20 @@ extension Date { } } +extension String { + var iso8601: Date? { + let formatter = DateFormatter.iso8061 + return formatter.date(from: self) + } + + var timeIntervalSince1970String: String? { + guard let timeInterval = self.iso8601?.timeIntervalSince1970 else { + return nil + } + return "\(timeInterval)" + } +} + public class ProductService { let db: DynamoDB @@ -78,18 +92,21 @@ public class ProductService { public func updateItem(product: Product) -> EventLoopFuture { var product = product let date = Date() + let updatedAt = "\(date.timeIntervalSince1970)" product.updatedAt = date.iso8601 let input = DynamoDB.UpdateItemInput( + conditionExpression: "attribute_exists(#createdAt)", expressionAttributeNames: [ "#name": Product.Field.name, "#description": Product.Field.description, "#updatedAt": Product.Field.updatedAt, + "#createdAt": Product.Field.createdAt ], expressionAttributeValues: [ ":name": DynamoDB.AttributeValue(s: product.name), ":description": DynamoDB.AttributeValue(s: product.description), - ":updatedAt": DynamoDB.AttributeValue(s: product.updatedAt), + ":updatedAt": DynamoDB.AttributeValue(n: updatedAt) ], key: [Product.Field.sku: DynamoDB.AttributeValue(s: product.sku)], returnValues: DynamoDB.ReturnValue.allNew,