├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── Readme.md ├── data ├── post1.json ├── post2.json ├── post3.json └── put3.json ├── functions ├── delete.go ├── get.go ├── list-by-year.go ├── moviedao.go ├── post.go └── put.go ├── go-crud.iml ├── img ├── allPostsDynamoDBTable.jpg ├── firstDeleteDynamoDBTable.jpg ├── firstPostDynamoDBTable.jpg ├── firstUpdateDynamoDBTable.jpg └── initialDynamoDBTable.jpg └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Serverless directories 2 | .serverless 3 | .DS_Store 4 | .idea 5 | 6 | # golang output binary directory 7 | bin 8 | 9 | # golang vendor (dependencies) directory 10 | vendor 11 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:d5039f72505274cddd25fef3b4790e7d985eaf063a0c7df1363ed6292ddc30fb" 6 | name = "github.com/aws/aws-lambda-go" 7 | packages = [ 8 | "events", 9 | "lambda", 10 | "lambda/messages", 11 | "lambdacontext", 12 | ] 13 | pruneopts = "" 14 | revision = "6e2e37798efbb1dfd8e9c6681702e683a6046517" 15 | version = "v1.0.1" 16 | 17 | [[projects]] 18 | digest = "1:ea5f610419c5dfdbf17c507bc449b81dafda953e94a003c9669bfa643767fe4f" 19 | name = "github.com/aws/aws-sdk-go" 20 | packages = [ 21 | "aws", 22 | "aws/awserr", 23 | "aws/awsutil", 24 | "aws/client", 25 | "aws/client/metadata", 26 | "aws/corehandlers", 27 | "aws/credentials", 28 | "aws/credentials/ec2rolecreds", 29 | "aws/credentials/endpointcreds", 30 | "aws/credentials/stscreds", 31 | "aws/defaults", 32 | "aws/ec2metadata", 33 | "aws/endpoints", 34 | "aws/request", 35 | "aws/session", 36 | "aws/signer/v4", 37 | "internal/shareddefaults", 38 | "private/protocol", 39 | "private/protocol/json/jsonutil", 40 | "private/protocol/jsonrpc", 41 | "private/protocol/query", 42 | "private/protocol/query/queryutil", 43 | "private/protocol/rest", 44 | "private/protocol/xml/xmlutil", 45 | "service/dynamodb", 46 | "service/dynamodb/dynamodbattribute", 47 | "service/dynamodb/expression", 48 | "service/sts", 49 | ] 50 | pruneopts = "" 51 | revision = "788eb15477cf129a45d8e1d0af5f533348f4125d" 52 | version = "v1.12.78" 53 | 54 | [[projects]] 55 | digest = "1:a00483fe4106b86fb1187a92b5cf6915c85f294ed4c129ccbe7cb1f1a06abd46" 56 | name = "github.com/go-ini/ini" 57 | packages = ["."] 58 | pruneopts = "" 59 | revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a" 60 | version = "v1.32.0" 61 | 62 | [[projects]] 63 | digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e" 64 | name = "github.com/jmespath/go-jmespath" 65 | packages = ["."] 66 | pruneopts = "" 67 | revision = "0b12d6b5" 68 | 69 | [solve-meta] 70 | analyzer-name = "dep" 71 | analyzer-version = 1 72 | input-imports = [ 73 | "github.com/aws/aws-lambda-go/events", 74 | "github.com/aws/aws-lambda-go/lambda", 75 | "github.com/aws/aws-sdk-go/aws", 76 | "github.com/aws/aws-sdk-go/aws/session", 77 | "github.com/aws/aws-sdk-go/service/dynamodb", 78 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute", 79 | "github.com/aws/aws-sdk-go/service/dynamodb/expression", 80 | ] 81 | solver-name = "gps-cdcl" 82 | solver-version = 1 83 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | 22 | 23 | [[constraint]] 24 | name = "github.com/aws/aws-lambda-go" 25 | version = "^1.0.1" 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | go_apps = bin/get bin/post bin/delete bin/put bin/list-by-year 2 | 3 | bin/% : functions/%.go functions/moviedao.go 4 | env GOOS=linux go build -ldflags="-s -w" -o $@ $< functions/moviedao.go 5 | 6 | build: $(go_apps) | vendor 7 | 8 | vendor: Gopkg.toml 9 | dep ensure 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # go-sls-crudl 2 | 3 | This project riffs off of the [Dynamo DB Golang samples](https://github.com/awsdocs/aws-doc-sdk-examples/tree/master/go/example_code/dynamodb) and the [Serverless Framework Go example](https://serverless.com/blog/framework-example-golang-lambda-support/) to create an example of how to build a simple API Gateway -> Lambda -> DynamoDB set of methods. 4 | 5 | ## Code Organization 6 | Note that, instead of using the `create_table.go` to set up the initial table like the AWS code example does, the resource building mechanism that Serverless provides is used. Individual code is organized as follows: 7 | 8 | * functions/post.do - POST method for creating a new item 9 | * functions/get.do - GET method for reading a specific item 10 | * functions/delete.do - DELETE method for deleting a specific item 11 | * functions/put.do - PUT method for updating an existing item 12 | * functions/list-by-year.do - GET method for listing all or a subset of items 13 | * img/* - Images of DynamoDB tables to make this Readme easier to follow 14 | * moviedao/moviedao.do - DAO wrapper around the DynamoDB calls 15 | * data/XXX.json - Set of sample data files for POST and PUT actions 16 | * Makefile - Used for dep package management and compiles of individual functions 17 | * serverless.yml - Defines the initial table, function defs, and API Gateway events 18 | 19 | Note that given the recency of Golang support on both AWS Lambda and the Serverless Framework, combined with my own Go noob-ness, I'm not entirely certain this is the best layout but it was functional. My hope is that it helps spark a healthy debate over what a Go Serverless project should look like. 20 | 21 | ## Set Up 22 | If you are a Serverless Framework rookie, [follow the installation instructions here](https://serverless.com/blog/anatomy-of-a-serverless-app/#setup). If you are a grizzled vet, be sure that you have v1.26 or later as that's the version that introduces Golang support. You'll also need to [install Go](https://golang.org/doc/install) and it's dependency manager, [dep](https://github.com/golang/dep). 23 | 24 | When both of those tasks are done, cd into your `GOPATH` (more than likely ~/go/src/) and clone this project into that folder. Then cd into the resulting `go-sls-crudl` folder and compile the source with `make`: 25 | 26 | ```bash 27 | $ make 28 | dep ensure 29 | env GOOS=linux go build -ldflags="-s -w" -o bin/get functions/get.go 30 | env GOOS=linux go build -ldflags="-s -w" -o bin/post functions/post.go 31 | env GOOS=linux go build -ldflags="-s -w" -o bin/delete functions/delete.go 32 | env GOOS=linux go build -ldflags="-s -w" -o bin/put functions/put.go 33 | env GOOS=linux go build -ldflags="-s -w" -o bin/list-by-year functions/list-by-year.go 34 | ``` 35 | What is this makefile doing? First, it runs the `dep ensure` command, which will scan your underlying .go files looking for dependencies to install, which it will grab off of Github as needed and place under a newly created `vendor` folder under `go-sls-crudl`. Then, it'll compile the individual function files, placing the resulting binaries in the `bin` folder. 36 | 37 | If you look at the `serverless.yml` file, it makes references to those recently compiled function binaries, one for each function in our service and each one corresponding to a different verb/path in our API we're creating. Deploy the entire service with the 'sls' command: 38 | 39 | ```bash 40 | $ sls deploy 41 | Serverless: Packaging service... 42 | Serverless: Excluding development dependencies... 43 | Serverless: Creating Stack... 44 | Serverless: Checking Stack create progress... 45 | ..... 46 | Serverless: Stack create finished... 47 | Serverless: Uploading CloudFormation file to S3... 48 | Serverless: Uploading artifacts... 49 | Serverless: Uploading service .zip file to S3 (15.19 MB)... 50 | Serverless: Validating template... 51 | Serverless: Updating Stack... 52 | Serverless: Checking Stack update progress... 53 | ................................................................................................... 54 | Serverless: Stack update finished... 55 | Service Information 56 | service: go-sls-crudl 57 | stage: dev 58 | region: us-east-1 59 | stack: go-sls-crudl-dev 60 | api keys: 61 | None 62 | endpoints: 63 | GET - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/movies/{year} 64 | GET - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/movies/{year}/{title} 65 | POST - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/movies 66 | DELETE - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/movies/{year}/{title} 67 | PUT - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/movies 68 | functions: 69 | list: go-sls-crudl-dev-list 70 | get: go-sls-crudl-dev-get 71 | post: go-sls-crudl-dev-post 72 | delete: go-sls-crudl-dev-delete 73 | put: go-sls-crudl-dev-put 74 | ``` 75 | 76 | When done, you can find the new DynamoDB table in the AWS Console, which should initially look like this: 77 | 78 | ![Initial DynamoDB Table](/img/initialDynamoDBTable.jpg) 79 | 80 | and your `` will be of the format 'https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev/movies' where `XXXXXXXXXX` will be some random string generated by AWS. 81 | 82 | The development cycle would then be: 83 | 84 | * Make changes to the .go files 85 | * Run `make` to compile the binaries 86 | * Run `sls deploy` to construct the service and push changes to lambda 87 | * Use `curl` to then interrogate the API as described below 88 | 89 | ## Using 90 | Once deployed and substituting your `` the following CURL commands can be used to interact with the resulting API, whose results can be confirmed in the DynamoDB console 91 | 92 | ### POST 93 | 94 | ```bash 95 | curl -X POST https: -d @data/post1.json 96 | ``` 97 | Which should result in the DynamoDB table looking like this: 98 | 99 | ![First Post DynamoDB Table](/img/firstPostDynamoDBTable.jpg) 100 | 101 | Rinse/repeat for other data files to yeild: 102 | 103 | ![All Posts DynamoDB Table](/img/allPostsDynamoDBTable.jpg) 104 | 105 | ### GET Specific Item 106 | Using the year and title (replacing spaces wiht '-' or '+'), you can now obtain an item as follows (prettified output): 107 | ```bash 108 | curl https:///2013/Hunger-Games-Catching-Fire 109 | { 110 | "year": 2013, 111 | "title": "Hunger Games Catching Fire", 112 | "info": { 113 | "plot": "Katniss Everdeen and Peeta Mellark become targets of the Capitol after their victory in the 74th Hunger Games sparks a rebellion in the Districts of Panem.", 114 | "rating": 7.6 115 | } 116 | } 117 | ``` 118 | 119 | ### GET a List of Items 120 | You can list items by year as follows (prettified output): 121 | ```bash 122 | curl https:///2013 123 | [ 124 | { 125 | "year": 2013, 126 | "title": "Hunger Games Catching Fire", 127 | "info": { 128 | "plot": "", 129 | "rating": 0 130 | } 131 | }, 132 | { 133 | "year": 2013, 134 | "title": "Turn It Down Or Else", 135 | "info": { 136 | "plot": "", 137 | "rating": 0 138 | } 139 | } 140 | ] 141 | ``` 142 | 143 | ### DELETE Specific Item 144 | Using the same year and title specifiers, you can delete as follows: 145 | ```bash 146 | curl -X DELETE https:///2013/Hunger-Games-Catching-Fire 147 | ``` 148 | Which should result in the DynamoDB table looking like this: 149 | 150 | ![First Delete DynamoDB Table](/img/firstDeleteDynamoDBTable.jpg) 151 | 152 | ### UPDATE Specific Item 153 | You can update as follows: 154 | ```bash 155 | curl -X PUT https: -d @data/put3.json 156 | ``` 157 | Which should result in the DynamoDB table looking like this: 158 | 159 | ![First Update DynamoDB Table](/img/firstUpdateDynamoDBTable.jpg) 160 | -------------------------------------------------------------------------------- /data/post1.json: -------------------------------------------------------------------------------- 1 | { 2 | "year" : 2013, 3 | "title" : "Turn It Down, Or Else!", 4 | "info" : { 5 | "directors" : [ 6 | "Alice Smith", 7 | "Bob Jones" 8 | ], 9 | "release_date" : "2013-01-18T00:00:00Z", 10 | "rating" : 6.2, 11 | "genres" : [ 12 | "Comedy", 13 | "Drama" 14 | ], 15 | "image_url" : "http://ia.media-imdb.com/images/N/O9ERWAU7FS797AJ7LU8HN09AMUP908RLlo5JF90EWR7LJKQ7@@._V1_SX400_.jpg", 16 | "plot" : "A rock band plays their music at high volumes, annoying the neighbors.", 17 | "rank" : 11, 18 | "running_time_secs" : 5215, 19 | "actors" : [ 20 | "David Matthewman", 21 | "Ann Thomas", 22 | "Jonathan G. Neff" 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /data/post2.json: -------------------------------------------------------------------------------- 1 | { 2 | "year" : 2013, 3 | "title" : "Hunger Games: Catching Fire", 4 | "info" : { 5 | "directors" : [ 6 | "Francis Lawrence" 7 | ], 8 | "release_date" : "2013-11-22T00:00:00Z", 9 | "rating" : 7.6, 10 | "genres" : [ 11 | "Action", 12 | "Adventure" 13 | ], 14 | "image_url" : "http://ia.media-imdb.com/images/N/O9ERWAU7FS797AJ7LU8HN09AMUP908RLlo5JF90EWR7LJKQ7@@._V1_SX400_.jpg", 15 | "plot" : "Katniss Everdeen and Peeta Mellark become targets of the Capitol after their victory in the 74th Hunger Games sparks a rebellion in the Districts of Panem.", 16 | "rank" : 1, 17 | "running_time_secs" : 8760, 18 | "actors" : [ 19 | "Jennifer Lawrence", 20 | "Josh Hutcherson", 21 | "Liam Hemsworth" 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /data/post3.json: -------------------------------------------------------------------------------- 1 | { 2 | "year": 2015, 3 | "title": "The Big New Movie", 4 | "info": { 5 | "plot": "Nothing happens at all.", 6 | "rating": 0 7 | } 8 | } -------------------------------------------------------------------------------- /data/put3.json: -------------------------------------------------------------------------------- 1 | { 2 | "year": 2015, 3 | "title": "The Big New Movie", 4 | "info": { 5 | "plot": "OK, some things happen.", 6 | "rating": 3 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /functions/delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/aws/aws-lambda-go/lambda" 9 | ) 10 | 11 | type Response struct { 12 | Message string `json:"message"` 13 | } 14 | 15 | // Parse slug into a space separated string 16 | func parseSlug(orig string) (retval string) { 17 | retval = strings.Replace(orig, "-", " ", -1) 18 | retval = strings.Replace(retval, "+", " ", -1) 19 | return retval 20 | } 21 | 22 | func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 23 | // Make the call to the DAO with params found in the path 24 | fmt.Println("Path vars: ", request.PathParameters["year"], " ", parseSlug(request.PathParameters["title"])) 25 | err := Delete(request.PathParameters["year"], parseSlug(request.PathParameters["title"])) 26 | if err != nil { 27 | panic(fmt.Sprintf("Failed to find Item, %v", err)) 28 | } 29 | return events.APIGatewayProxyResponse{Body: "Success\n", StatusCode: 200}, nil 30 | } 31 | 32 | func main() { 33 | lambda.Start(Handler) 34 | } 35 | -------------------------------------------------------------------------------- /functions/get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | "github.com/aws/aws-lambda-go/lambda" 10 | ) 11 | 12 | type Response struct { 13 | Message string `json:"message"` 14 | } 15 | 16 | // Parse slug into a space separated string 17 | func parseSlug(orig string) (retval string) { 18 | retval = strings.Replace(orig, "-", " ", -1) 19 | retval = strings.Replace(retval, "+", " ", -1) 20 | return retval 21 | } 22 | 23 | func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 24 | // Make the call to the DAO with params found in the path 25 | fmt.Println("Path vars: ", request.PathParameters["year"], " ", parseSlug(request.PathParameters["title"])) 26 | item, err := GetByYearTitle(request.PathParameters["year"], parseSlug(request.PathParameters["title"])) 27 | if err != nil { 28 | panic(fmt.Sprintf("Failed to find Item, %v", err)) 29 | } 30 | 31 | // Make sure the Item isn't empty 32 | if item.Year <= 0 { 33 | fmt.Println("Could not find movie") 34 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 500}, nil 35 | } 36 | 37 | // Log and return result 38 | jsonItem, _ := json.Marshal(item) 39 | stringItem := string(jsonItem) + "\n" 40 | fmt.Println("Found item: ", stringItem) 41 | return events.APIGatewayProxyResponse{Body: stringItem, StatusCode: 200}, nil 42 | } 43 | 44 | func main() { 45 | lambda.Start(Handler) 46 | } 47 | -------------------------------------------------------------------------------- /functions/list-by-year.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/aws/aws-lambda-go/lambda" 9 | ) 10 | 11 | type Response struct { 12 | Message string `json:"message"` 13 | } 14 | 15 | func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 16 | // Make the call to the DAO with params found in the path 17 | fmt.Println("Path vars: ", request.PathParameters["year"]) 18 | items, err := ListByYear(request.PathParameters["year"]) 19 | if err != nil { 20 | panic(fmt.Sprintf("Failed to find Item, %v", err)) 21 | } 22 | 23 | // Make sure the Item isn't empty 24 | if len(items) == 0 { 25 | fmt.Println("Could not find movies with year ", request.PathParameters["year"]) 26 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 500}, nil 27 | } 28 | 29 | // Log and return result 30 | stringItems := "[" 31 | for i := 0; i < len(items); i++ { 32 | jsonItem, _ := json.Marshal(items[i]) 33 | stringItems += string(jsonItem) 34 | if i != len(items)-1 { 35 | stringItems += ",\n" 36 | } 37 | } 38 | stringItems += "]\n" 39 | fmt.Println("Found items: ", stringItems) 40 | return events.APIGatewayProxyResponse{Body: stringItems, StatusCode: 200}, nil 41 | } 42 | 43 | func main() { 44 | lambda.Start(Handler) 45 | } 46 | -------------------------------------------------------------------------------- /functions/moviedao.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/dynamodb" 13 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 14 | "github.com/aws/aws-sdk-go/service/dynamodb/expression" 15 | ) 16 | 17 | // ItemInfo has more data for our movie item 18 | type ItemInfo struct { 19 | Plot string `json:"plot"` 20 | Rating float64 `json:"rating"` 21 | } 22 | 23 | // Item has fields for the DynamoDB keys (Year and Title) and an ItemInfo for more data 24 | type Item struct { 25 | Year int `json:"year"` 26 | Title string `json:"title"` 27 | Info ItemInfo `json:"info"` 28 | } 29 | 30 | // GetByYearTitle wraps up the DynamoDB calls to fetch a specific Item 31 | // Based on https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/dynamodb/read_item.go 32 | func GetByYearTitle(year, title string) (Item, error) { 33 | // Build the Dynamo client object 34 | sess := session.Must(session.NewSession()) 35 | svc := dynamodb.New(sess) 36 | item := Item{} 37 | 38 | // Perform the query 39 | fmt.Println("Trying to read from table: ", os.Getenv("TABLE_NAME")) 40 | result, err := svc.GetItem(&dynamodb.GetItemInput{ 41 | TableName: aws.String(os.Getenv("TABLE_NAME")), 42 | Key: map[string]*dynamodb.AttributeValue{ 43 | "year": { 44 | N: aws.String(year), 45 | }, 46 | "title": { 47 | S: aws.String(title), 48 | }, 49 | }, 50 | }) 51 | if err != nil { 52 | fmt.Println(err.Error()) 53 | return item, err 54 | } 55 | 56 | // Unmarshall the result in to an Item 57 | err = dynamodbattribute.UnmarshalMap(result.Item, &item) 58 | if err != nil { 59 | fmt.Println(err.Error()) 60 | return item, err 61 | } 62 | 63 | return item, nil 64 | } 65 | 66 | // ListByYear wraps up the DynamoDB calls to list all items of a particular year 67 | // Based on https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/dynamodb/scan_items.go 68 | func ListByYear(year string) ([]Item, error) { 69 | // Build the Dynamo client object 70 | sess := session.Must(session.NewSession()) 71 | svc := dynamodb.New(sess) 72 | items := []Item{} 73 | 74 | // Create the Expression to fill the input struct with. 75 | yearAsInt, err := strconv.Atoi(year) 76 | filt := expression.Name("year").Equal(expression.Value(yearAsInt)) 77 | 78 | // Get back the title, year, and rating 79 | proj := expression.NamesList(expression.Name("title"), expression.Name("year")) 80 | 81 | expr, err := expression.NewBuilder().WithFilter(filt).WithProjection(proj).Build() 82 | 83 | if err != nil { 84 | fmt.Println("Got error building expression:") 85 | fmt.Println(err.Error()) 86 | return items, err 87 | } 88 | 89 | // Build the query input parameters 90 | params := &dynamodb.ScanInput{ 91 | ExpressionAttributeNames: expr.Names(), 92 | ExpressionAttributeValues: expr.Values(), 93 | FilterExpression: expr.Filter(), 94 | ProjectionExpression: expr.Projection(), 95 | TableName: aws.String(os.Getenv("TABLE_NAME")), 96 | } 97 | 98 | // Make the DynamoDB Query API call 99 | result, err := svc.Scan(params) 100 | fmt.Println("Result", result) 101 | 102 | if err != nil { 103 | fmt.Println("Query API call failed:") 104 | fmt.Println((err.Error())) 105 | return items, err 106 | } 107 | 108 | numItems := 0 109 | for _, i := range result.Items { 110 | item := Item{} 111 | 112 | err = dynamodbattribute.UnmarshalMap(i, &item) 113 | 114 | if err != nil { 115 | fmt.Println("Got error unmarshalling:") 116 | fmt.Println(err.Error()) 117 | return items, err 118 | } 119 | 120 | fmt.Println("Title: ", item.Title) 121 | items = append(items, item) 122 | numItems++ 123 | } 124 | 125 | fmt.Println("Found", numItems, "movie(s) in year ", year) 126 | if err != nil { 127 | fmt.Println(err.Error()) 128 | return items, err 129 | } 130 | 131 | return items, nil 132 | } 133 | 134 | // Post extracts the Item JSON and writes it to DynamoDB 135 | // Based on https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/dynamodb/create_item.go 136 | func Post(body string) (Item, error) { 137 | // Create the dynamo client object 138 | sess := session.Must(session.NewSession()) 139 | svc := dynamodb.New(sess) 140 | 141 | // Marshall the requrest body 142 | var thisItem Item 143 | json.Unmarshal([]byte(body), &thisItem) 144 | 145 | // Take out non-alphanumberic except space characters from the title for easier slug building on reads 146 | reg, err := regexp.Compile("[^a-zA-Z0-9\\s]+") 147 | thisItem.Title = reg.ReplaceAllString(thisItem.Title, "") 148 | fmt.Println("Item to add:", thisItem) 149 | 150 | // Marshall the Item into a Map DynamoDB can deal with 151 | av, err := dynamodbattribute.MarshalMap(thisItem) 152 | if err != nil { 153 | fmt.Println("Got error marshalling map:") 154 | fmt.Println(err.Error()) 155 | return thisItem, err 156 | } 157 | 158 | // Create Item in table and return 159 | input := &dynamodb.PutItemInput{ 160 | Item: av, 161 | TableName: aws.String(os.Getenv("TABLE_NAME")), 162 | } 163 | _, err = svc.PutItem(input) 164 | return thisItem, err 165 | 166 | } 167 | 168 | // Delete wraps up the DynamoDB calls to delete a specific Item 169 | // Based on https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/dynamodb/delete_item.go 170 | func Delete(year, title string) error { 171 | // Build the Dynamo client object 172 | sess := session.Must(session.NewSession()) 173 | svc := dynamodb.New(sess) 174 | 175 | // Perform the delete 176 | input := &dynamodb.DeleteItemInput{ 177 | Key: map[string]*dynamodb.AttributeValue{ 178 | "year": { 179 | N: aws.String(year), 180 | }, 181 | "title": { 182 | S: aws.String(title), 183 | }, 184 | }, 185 | TableName: aws.String(os.Getenv("TABLE_NAME")), 186 | } 187 | 188 | _, err := svc.DeleteItem(input) 189 | if err != nil { 190 | fmt.Println(err.Error()) 191 | return err 192 | } 193 | return nil 194 | } 195 | 196 | // Put extracts the Item JSON and updates it in DynamoDB 197 | // Based on https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/dynamodb/update_item.go 198 | func Put(body string) (Item, error) { 199 | // Create the dynamo client object 200 | sess := session.Must(session.NewSession()) 201 | svc := dynamodb.New(sess) 202 | 203 | // Marshall the requrest body 204 | var thisItem Item 205 | json.Unmarshal([]byte(body), &thisItem) 206 | 207 | // Take out non-alphanumberic except space characters from the title for easier slug building on reads 208 | reg, err := regexp.Compile("[^a-zA-Z0-9\\s]+") 209 | thisItem.Title = reg.ReplaceAllString(thisItem.Title, "") 210 | fmt.Println("Item to update:", thisItem) 211 | 212 | // Update Item in table and return 213 | input := &dynamodb.UpdateItemInput{ 214 | ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ 215 | ":r": { 216 | N: aws.String(strconv.FormatFloat(thisItem.Info.Rating, 'f', 1, 64)), 217 | }, 218 | ":p": { 219 | S: aws.String(thisItem.Info.Plot), 220 | }, 221 | }, 222 | TableName: aws.String(os.Getenv("TABLE_NAME")), 223 | Key: map[string]*dynamodb.AttributeValue{ 224 | "year": { 225 | N: aws.String(strconv.Itoa(thisItem.Year)), 226 | }, 227 | "title": { 228 | S: aws.String(thisItem.Title), 229 | }, 230 | }, 231 | ReturnValues: aws.String("UPDATED_NEW"), 232 | UpdateExpression: aws.String("set info.rating = :r, info.plot = :p"), 233 | } 234 | 235 | _, err = svc.UpdateItem(input) 236 | if err != nil { 237 | fmt.Println(err.Error()) 238 | } 239 | return thisItem, err 240 | 241 | } 242 | -------------------------------------------------------------------------------- /functions/post.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | ) 9 | 10 | func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 11 | // Log body and pass to the DAO 12 | fmt.Println("Received body: ", request.Body) 13 | item, err := Post(request.Body) 14 | if err != nil { 15 | fmt.Println("Got error calling post") 16 | fmt.Println(err.Error()) 17 | return events.APIGatewayProxyResponse{Body: "Error", StatusCode: 500}, nil 18 | } 19 | 20 | // Log and return result 21 | fmt.Println("Wrote item: ", item) 22 | return events.APIGatewayProxyResponse{Body: "Success\n", StatusCode: 200}, nil 23 | } 24 | 25 | func main() { 26 | lambda.Start(Handler) 27 | } 28 | -------------------------------------------------------------------------------- /functions/put.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | ) 9 | 10 | func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 11 | // Log body and pass to the DAO 12 | fmt.Println("Received body: ", request.Body) 13 | item, err := Put(request.Body) 14 | if err != nil { 15 | fmt.Println("Got error calling put") 16 | fmt.Println(err.Error()) 17 | return events.APIGatewayProxyResponse{Body: "Error", StatusCode: 500}, nil 18 | } 19 | 20 | // Log and return result 21 | fmt.Println("Updated item: ", item) 22 | return events.APIGatewayProxyResponse{Body: "Success\n", StatusCode: 200}, nil 23 | } 24 | 25 | func main() { 26 | lambda.Start(Handler) 27 | } 28 | -------------------------------------------------------------------------------- /go-crud.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /img/allPostsDynamoDBTable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdguru/go-sls-crudl/47593d724dfbf28d7ac40c2bca0e401320dcd4e4/img/allPostsDynamoDBTable.jpg -------------------------------------------------------------------------------- /img/firstDeleteDynamoDBTable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdguru/go-sls-crudl/47593d724dfbf28d7ac40c2bca0e401320dcd4e4/img/firstDeleteDynamoDBTable.jpg -------------------------------------------------------------------------------- /img/firstPostDynamoDBTable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdguru/go-sls-crudl/47593d724dfbf28d7ac40c2bca0e401320dcd4e4/img/firstPostDynamoDBTable.jpg -------------------------------------------------------------------------------- /img/firstUpdateDynamoDBTable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdguru/go-sls-crudl/47593d724dfbf28d7ac40c2bca0e401320dcd4e4/img/firstUpdateDynamoDBTable.jpg -------------------------------------------------------------------------------- /img/initialDynamoDBTable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerdguru/go-sls-crudl/47593d724dfbf28d7ac40c2bca0e401320dcd4e4/img/initialDynamoDBTable.jpg -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: go-sls-crudl 2 | 3 | provider: 4 | name: aws 5 | runtime: go1.x 6 | environment: 7 | TABLE_NAME: movies-${opt:stage, self:provider.stage} 8 | iamRoleStatements: 9 | - Effect: Allow 10 | Action: 11 | - dynamodb:DescribeTable 12 | - dynamodb:Query 13 | - dynamodb:Scan 14 | - dynamodb:GetItem 15 | - dynamodb:PutItem 16 | - dynamodb:UpdateItem 17 | - dynamodb:DeleteItem 18 | Resource: "arn:aws:dynamodb:*:*:*" 19 | 20 | package: 21 | exclude: 22 | - ./** 23 | include: 24 | - ./bin/** 25 | 26 | functions: 27 | list: 28 | handler: bin/list-by-year 29 | events: 30 | - http: 31 | path: movies/{year} 32 | method: get 33 | get: 34 | handler: bin/get 35 | events: 36 | - http: 37 | path: movies/{year}/{title} 38 | method: get 39 | post: 40 | handler: bin/post 41 | events: 42 | - http: 43 | path: movies 44 | method: post 45 | delete: 46 | handler: bin/delete 47 | events: 48 | - http: 49 | path: movies/{year}/{title} 50 | method: delete 51 | put: 52 | handler: bin/put 53 | events: 54 | - http: 55 | path: movies 56 | method: put 57 | 58 | resources: 59 | Resources: 60 | GoCrudDynamoDbTable: 61 | Type: 'AWS::DynamoDB::Table' 62 | DeletionPolicy: Retain 63 | Properties: 64 | AttributeDefinitions: 65 | - AttributeName: year 66 | AttributeType: N 67 | - AttributeName: title 68 | AttributeType: S 69 | KeySchema: 70 | - AttributeName: year 71 | KeyType: HASH 72 | - AttributeName: title 73 | KeyType: RANGE 74 | ProvisionedThroughput: 75 | ReadCapacityUnits: 1 76 | WriteCapacityUnits: 1 77 | TableName: 'movies-${opt:stage, self:provider.stage}' 78 | --------------------------------------------------------------------------------