diff --git a/Examples/LambdaFunctions/Package.swift b/Examples/LambdaFunctions/Package.swift index ae79d287..d849ce4e 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: "ProductAPI", targets: ["ProductAPI"]), ], 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: "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"), + .product(name: "Logging", package: "swift-log"), + ] + ) ] ) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift new file mode 100644 index 00000000..2a278a05 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/APIGateway+Extensions.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import class Foundation.JSONEncoder +import class Foundation.JSONDecoder + +extension APIGateway.V2.Request { + + 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 Self.decoder.decode(T.self, from: dataBody) + } +} + +extension APIGateway.V2.Response { + + 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", + ] + + public init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) { + self.init( + statusCode: statusCode, + headers: APIGateway.V2.Response.defaultHeaders, + multiValueHeaders: nil, + body: "{\"message\":\"\(String(describing: error))\"}", + isBase64Encoded: false + ) + } + + public init(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) { + var body: String = "{}" + if let data = try? Self.encoder.encode(object) { + body = String(data: data, encoding: .utf8) ?? body + } + self.init( + statusCode: statusCode, + headers: APIGateway.V2.Response.defaultHeaders, + multiValueHeaders: nil, + body: body, + isBase64Encoded: false + ) + } +} diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift new file mode 100644 index 00000000..542b8db1 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSDynamoDB +import Foundation + +extension Product { + + public var dynamoDictionary: [String: DynamoDB.AttributeValue] { + var dictionary = [ + Field.sku: DynamoDB.AttributeValue(s: sku), + Field.name: DynamoDB.AttributeValue(s: name), + Field.description: DynamoDB.AttributeValue(s: description), + ] + if let createdAt = createdAt?.timeIntervalSince1970String { + dictionary[Field.createdAt] = DynamoDB.AttributeValue(n: createdAt) + } + + if let updatedAt = updatedAt?.timeIntervalSince1970String { + dictionary[Field.updatedAt] = DynamoDB.AttributeValue(n: updatedAt) + } + return dictionary + } + + public init(dictionary: [String: DynamoDB.AttributeValue]) throws { + 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 + 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/Product.swift b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift new file mode 100644 index 00000000..277c0bb2 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/Product.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// 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? + + 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/ProductLambda.swift b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift new file mode 100644 index 00000000..de1402a9 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSDynamoDB +import AWSLambdaEvents +import AWSLambdaRuntime +import AsyncHTTPClient +import Logging +import NIO + +enum Operation: String { + case create + case read + case update + case delete + case list +} + +struct EmptyResponse: Codable {} + +struct ProductLambda: LambdaHandler { + + 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 { + return .useast1 + } + } + + static func tableName() throws -> String { + guard let tableName = Lambda.env("PRODUCTS_TABLE_NAME") else { + throw APIError.tableNameNotFound + } + return tableName + } + + 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() + + 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), + configuration: configuration + ) + + self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient)) + self.tableName = try Self.tableName() + + self.service = ProductService( + db: db, + tableName: tableName + ) + } + + 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 new file mode 100644 index 00000000..18a33486 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductLambdaHandler.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaEvents +import AWSLambdaRuntime +import Logging +import NIO + +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 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) + } + } + + func createLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + guard let product: Product = try? event.bodyObject() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + 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) + } + } + + 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) + } + 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) + } + } + + func updateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) -> EventLoopFuture { + guard let product: Product = try? event.bodyObject() else { + let error = APIError.invalidRequest + return context.eventLoop.makeFailedFuture(error) + } + 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) + } + } + + 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) + } + 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) + } + } + + 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 new file mode 100644 index 00000000..d65bb992 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/ProductService.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSDynamoDB +import Foundation +import NIO + +public enum APIError: Error { + case invalidItem + case tableNameNotFound + case invalidRequest + case invalidHandler +} + +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) + } +} + +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 + 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() + product.createdAt = date.iso8601 + product.updatedAt = date.iso8601 + + let input = DynamoDB.PutItemInput( + item: product.dynamoDictionary, + tableName: tableName + ) + return db.putItem(input).flatMap { _ -> EventLoopFuture in + return self.readItem(key: product.sku) + } + } + + public func readItem(key: String) -> EventLoopFuture { + let input = DynamoDB.GetItemInput( + key: [Product.Field.sku: DynamoDB.AttributeValue(s: key)], + tableName: tableName + ) + return db.getItem(input).flatMapThrowing { data -> Product in + return try Product(dictionary: data.item ?? [:]) + } + } + + 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(n: updatedAt) + ], + key: [Product.Field.sku: DynamoDB.AttributeValue(s: product.sku)], + returnValues: DynamoDB.ReturnValue.allNew, + tableName: tableName, + updateExpression: "SET #name = :name, #description = :description, #updatedAt = :updatedAt" + ) + return db.updateItem(input).flatMap { _ -> EventLoopFuture in + return self.readItem(key: product.sku) + } + } + + public func deleteItem(key: String) -> EventLoopFuture { + let input = DynamoDB.DeleteItemInput( + key: [Product.Field.sku: DynamoDB.AttributeValue(s: key)], + tableName: tableName + ) + return db.deleteItem(input).map { _ in Void() } + } + + 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 ?? [] + } + } +} diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/main.swift b/Examples/LambdaFunctions/Sources/ProductAPI/main.swift new file mode 100644 index 00000000..b5ff1fa9 --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/main.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +Lambda.run(ProductLambda.init) diff --git a/Examples/LambdaFunctions/Sources/ProductAPI/swagger.json b/Examples/LambdaFunctions/Sources/ProductAPI/swagger.json new file mode 100644 index 00000000..76d4c54a --- /dev/null +++ b/Examples/LambdaFunctions/Sources/ProductAPI/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/ProductAPI-template.yml b/Examples/LambdaFunctions/scripts/SAM/ProductAPI-template.yml new file mode 100644 index 00000000..54610180 --- /dev/null +++ b/Examples/LambdaFunctions/scripts/SAM/ProductAPI-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/ProductAPI/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/ProductAPI-template.yml b/Examples/LambdaFunctions/scripts/serverless/ProductAPI-template.yml new file mode 100644 index 00000000..1198e8a5 --- /dev/null +++ b/Examples/LambdaFunctions/scripts/serverless/ProductAPI-template.yml @@ -0,0 +1,96 @@ +service: swift-awslambda-runtime-api + +# package: +# individually: true +package: + artifact: .build/lambda/ProductAPI/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