├── .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 | 
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 | 
100 |
101 | Rinse/repeat for other data files to yeild:
102 |
103 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------