├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bus └── event_bridge.go ├── domain ├── products.go ├── products_stream.go └── products_test.go ├── functions ├── delete-product │ ├── event.json │ └── main.go ├── get-product │ ├── event.json │ └── main.go ├── get-products │ └── main.go ├── products-stream │ ├── event.json │ └── main.go └── put-product │ ├── event.json │ └── main.go ├── go.mod ├── go.sum ├── handlers ├── apigateway.go └── dynamodb.go ├── imgs ├── diagram.png └── load-test.jpeg ├── integration-testing └── integration_test.go ├── load-testing ├── generator.js └── load-test.yml ├── store ├── dynamodb.go └── memory.go ├── template.yaml ├── tools ├── go.mod ├── go.sum └── tools.go └── types ├── bus.go ├── mocks └── mock_store.go ├── product.go └── store.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go: 12 | - "1.17" 13 | - "1.16" 14 | steps: 15 | - name: Set up Go ${{ matrix.go }} 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go }} 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Cache go modules 25 | uses: actions/cache@v2 26 | with: 27 | path: | 28 | ~/.cache/go-build 29 | ~/go/pkg/mod 30 | key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} 31 | restore-keys: | 32 | ${{ runner.os }}-go-${{ matrix.go }} 33 | 34 | - name: Run linter 35 | if: matrix.go == '1.17' 36 | run: make lint 37 | 38 | - name: Run tests 39 | run: go test -tags=unit -race -coverprofile=coverage.txt -covermode=atomic ./... 40 | 41 | - name: Upload coverage to Codecov 42 | uses: codecov/codecov-action@v2 43 | if: matrix.go == '1.17' 44 | with: 45 | files: ./coverage.txt 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .aws-sam 2 | .vscode 3 | bootstrap 4 | env-vars.json 5 | samconfig.toml 6 | bin 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | STACK_NAME ?= serverless-go-demo 2 | FUNCTIONS := get-products get-product put-product delete-product products-stream 3 | REGION := eu-central-1 4 | 5 | # To try different version of Go 6 | GO := go 7 | 8 | # Make sure to install aarch64 GCC compilers if you want to compile with GCC. 9 | CC := aarch64-linux-gnu-gcc 10 | GCCGO := aarch64-linux-gnu-gccgo-10 11 | 12 | ci: build tests-unit 13 | 14 | build: 15 | ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-${function}) 16 | 17 | build-%: 18 | cd functions/$* && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 ${GO} build -o bootstrap 19 | 20 | build-gcc: 21 | ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-gcc-${function}) 22 | 23 | build-gcc-%: 24 | cd functions/$* && GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=${CC} ${GO} build -o bootstrap 25 | 26 | build-gcc-optimized: 27 | ${MAKE} ${MAKEOPTS} $(foreach function,${FUNCTIONS}, build-gcc-optimized-${function}) 28 | 29 | build-gcc-optimized-%: 30 | cd functions/$* && GOOS=linux GOARCH=arm64 GCCGO=${GCCGO} ${GO} build -compiler gccgo -gccgoflags '-static -Ofast -march=armv8.2-a+fp16+rcpc+dotprod+crypto -mtune=neoverse-n1 -moutline-atomics' -o bootstrap 31 | 32 | invoke: 33 | @sam local invoke --env-vars env-vars.json GetProductsFunction 34 | 35 | invoke-put: 36 | @sam local invoke --env-vars env-vars.json --event functions/put-product/event.json PutProductFunction 37 | 38 | invoke-get: 39 | @sam local invoke --env-vars env-vars.json --event functions/get-product/event.json GetProductFunction 40 | 41 | invoke-delete: 42 | @sam local invoke --env-vars env-vars.json --event functions/delete-product/event.json DeleteProductFunction 43 | 44 | invoke-stream: 45 | @sam local invoke --env-vars env-vars.json --event functions/products-stream/event.json DDBStreamsFunction 46 | 47 | clean: 48 | @rm $(foreach function,${FUNCTIONS}, functions/${function}/bootstrap) 49 | 50 | deploy: 51 | if [ -f samconfig.toml ]; \ 52 | then sam deploy --stack-name ${STACK_NAME}; \ 53 | else sam deploy -g --stack-name ${STACK_NAME}; \ 54 | fi 55 | 56 | tests-unit: 57 | @go test -v -tags=unit -bench=. -benchmem -cover ./... 58 | 59 | tests-integ: 60 | API_URL=$$(aws cloudformation describe-stacks --stack-name $(STACK_NAME) \ 61 | --region $(REGION) \ 62 | --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \ 63 | --output text) go test -v -tags=integration ./... 64 | 65 | tests-load: 66 | API_URL=$$(aws cloudformation describe-stacks --stack-name $(STACK_NAME) \ 67 | --region $(REGION) \ 68 | --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \ 69 | --output text) artillery run load-testing/load-test.yml 70 | 71 | export GOBIN ?= $(shell pwd)/bin 72 | 73 | STATICCHECK = $(GOBIN)/staticcheck 74 | 75 | # Many Go tools take file globs or directories as arguments instead of packages 76 | GO_FILES := $(shell \ 77 | find . '(' -path '*/.*' -o -path './vendor' ')' -prune \ 78 | -o -name '*.go' -print | cut -b3-) 79 | MODULE_DIRS = . 80 | 81 | .PHONY: lint 82 | lint: $(STATICCHECK) 83 | @rm -rf lint.log 84 | @echo "Checking formatting..." 85 | @gofmt -d -s $(GO_FILES) 2>&1 | tee lint.log 86 | @echo "Checking vet..." 87 | @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go vet ./... 2>&1) &&) true | tee -a lint.log 88 | @echo "Checking staticcheck..." 89 | @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log 90 | @echo "Checking for unresolved FIXMEs..." 91 | @git grep -i fixme | grep -v -e Makefile | tee -a lint.log 92 | @[ ! -s lint.log ] 93 | @rm lint.log 94 | @echo "Checking 'go mod tidy'..." 95 | @make tidy 96 | @if ! git diff --quiet; then \ 97 | echo "'go diff tidy' resulted in chnges or working tree is dirty:"; \ 98 | git --no-pager diff; \ 99 | fi 100 | 101 | $(STATICCHECK): 102 | cd tools && go install honnef.co/go/tools/cmd/staticcheck 103 | 104 | .PHONY: tidy 105 | tidy: 106 | @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go mod tidy) &&) true 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Serverless Go Demo 2 | 3 | ![build](https://github.com/aws-samples/serverless-go-demo/actions/workflows/ci.yml/badge.svg) 4 | [![codecov](https://codecov.io/gh/aws-samples/serverless-go-demo/branch/main/graph/badge.svg?token=TxHdfJjSxP)](https://codecov.io/gh/aws-samples/serverless-go-demo) 5 | 6 |

7 | Architecture diagram 8 |

9 | 10 | This is a simple serverless application built in Golang. It consists of an API Gateway backed by four Lambda functions and a DynamoDB table for storage. 11 | 12 | This single project will create [five different binaries](./functions), one for each Lambda function. It uses an [hexagonal architecture pattern](https://aws.amazon.com/blogs/compute/developing-evolutionary-architecture-with-aws-lambda/) to decouple the [entry points](./handlers), from the main [domain logic](./domain), the [storage component](./store), and the [event bus component](./bus). 13 | 14 | ## 🏗️ Deployment and testing 15 | 16 | ### Requirements 17 | 18 | * [Go](https://go.dev) 19 | * The [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) for deploying to the cloud 20 | * [Artillery](https://artillery.io/) for load-testing the application 21 | 22 | ### Commands 23 | 24 | You can use the following commands at the root of this repository to test, build, and deploy this project: 25 | 26 | ```bash 27 | # Run unit tests 28 | make tests-unit 29 | 30 | # Compile and prepare Lambda functions 31 | make build 32 | 33 | # Deploy the functions on AWS 34 | make deploy 35 | 36 | # Run integration tests against the API in the cloud 37 | make tests-integ 38 | ``` 39 | 40 | ## Load Test 41 | 42 | [Artillery](https://www.artillery.io/) is used to make 300 requests / second for 10 minutes to our API endpoints. You can run this 43 | with the following command: 44 | 45 | ```bash 46 | make tests-load 47 | ``` 48 | 49 | ### CloudWatch Logs Insights 50 | 51 | Using this CloudWatch Logs Insights query you can analyse the latency of the requests made to the Lambda functions. 52 | 53 | The query separates cold starts from other requests and then gives you p50, p90 and p99 percentiles. 54 | 55 | The times bellow were obtained while running the sample code with 128MB of RAM and arm64 architecture. 56 | 57 | ``` 58 | filter @type="REPORT" 59 | | fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldStart 60 | | stats count(*) as count, pct(duration, 50) as p50, pct(duration, 90) as p90, pct(duration, 99) as p99, max(duration) as max by coldStart 61 | ``` 62 | 63 | ![Load Test Results](imgs/load-test.jpeg) 64 | 65 | ## 👀 With other languages 66 | 67 | You can find implementations of this project in other languages here: 68 | 69 | * [⭐ Groovy](https://github.com/aws-samples/serverless-groovy-demo) 70 | * [☕ Java with GraalVM](https://github.com/aws-samples/serverless-graalvm-demo) 71 | * [🤖 Kotlin](https://github.com/aws-samples/serverless-kotlin-demo) 72 | * [🦀 Rust](https://github.com/aws-samples/serverless-rust-demo) 73 | * [🏗️ TypeScript](https://github.com/aws-samples/serverless-typescript-demo) 74 | * [🥅 .NET](https://github.com/aws-samples/serverless-dotnet-demo) 75 | 76 | ## Security 77 | 78 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 79 | 80 | ## License 81 | 82 | This library is licensed under the MIT-0 License. See the LICENSE file. 83 | -------------------------------------------------------------------------------- /bus/event_bridge.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "math" 7 | 8 | "github.com/aws-samples/serverless-go-demo/types" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents" 13 | cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchevents/types" 14 | ) 15 | 16 | type EventBridgeBus struct { 17 | client *cloudwatchevents.Client 18 | busName string 19 | } 20 | 21 | var _ types.Bus = (*EventBridgeBus)(nil) 22 | 23 | func NewEventBridgeBus(ctx context.Context, busName string) *EventBridgeBus { 24 | cfg, err := config.LoadDefaultConfig(ctx) 25 | if err != nil { 26 | log.Fatalf("unable to load SDK config, %v", err) 27 | } 28 | 29 | client := cloudwatchevents.NewFromConfig(cfg) 30 | 31 | return &EventBridgeBus{ 32 | client: client, 33 | busName: busName, 34 | } 35 | } 36 | 37 | func (e *EventBridgeBus) Put(ctx context.Context, events []types.Event) ([]types.FailedEvent, error) { 38 | failedBatchEvents, err := 39 | batchEvents(events, 10, func(batchEvents []types.Event) ([]types.FailedEvent, error) { 40 | eventBridgeEvents := make([]cloudwatchtypes.PutEventsRequestEntry, len(batchEvents)) 41 | 42 | for i, event := range batchEvents { 43 | eventBridgeEvent := cloudwatchtypes.PutEventsRequestEntry{ 44 | EventBusName: &e.busName, 45 | Source: aws.String(event.Source), 46 | Detail: aws.String(event.Detail), 47 | DetailType: aws.String(event.DetailType), 48 | Resources: event.Resources, 49 | } 50 | 51 | eventBridgeEvents[i] = eventBridgeEvent 52 | } 53 | 54 | result, err := e.client.PutEvents(ctx, &cloudwatchevents.PutEventsInput{ 55 | Entries: eventBridgeEvents, 56 | }) 57 | 58 | failedEvents := []types.FailedEvent{} 59 | if err != nil { 60 | return failedEvents, err 61 | } 62 | 63 | if result.FailedEntryCount > 0 { 64 | for i, entry := range result.Entries { 65 | if entry.EventId != nil { 66 | continue 67 | } 68 | 69 | failedEvent := types.FailedEvent{ 70 | Event: batchEvents[i], 71 | FailureCode: *entry.ErrorCode, 72 | FailureMessage: *entry.ErrorMessage, 73 | } 74 | 75 | failedEvents = append(failedEvents, failedEvent) 76 | } 77 | } 78 | 79 | return failedEvents, nil 80 | }) 81 | 82 | return failedBatchEvents, err 83 | } 84 | 85 | func batchEvents(events []types.Event, maxBatchSize uint, batchFn func([]types.Event) ([]types.FailedEvent, error)) ([]types.FailedEvent, error) { 86 | skip := 0 87 | recordsAmount := len(events) 88 | batchAmount := int(math.Ceil(float64(recordsAmount) / float64(maxBatchSize))) 89 | 90 | batchFailedEvents := []types.FailedEvent{} 91 | 92 | for i := 0; i < batchAmount; i++ { 93 | lowerBound := skip 94 | upperBound := skip + int(maxBatchSize) 95 | 96 | if upperBound > recordsAmount { 97 | upperBound = recordsAmount 98 | } 99 | 100 | batchEvents := events[lowerBound:upperBound] 101 | skip += int(maxBatchSize) 102 | 103 | failedEvents, err := batchFn(batchEvents) 104 | if err != nil { 105 | return batchFailedEvents, err 106 | } 107 | 108 | if len(failedEvents) > 0 { 109 | batchFailedEvents = append(batchFailedEvents, failedEvents...) 110 | } 111 | } 112 | 113 | return batchFailedEvents, nil 114 | } 115 | -------------------------------------------------------------------------------- /domain/products.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/aws-samples/serverless-go-demo/types" 11 | ) 12 | 13 | var ( 14 | ErrJsonUnmarshal = errors.New("failed to parse product from request body") 15 | ErrProductIdMismatch = errors.New("product ID in path does not match product ID in body") 16 | ) 17 | 18 | type Products struct { 19 | store types.Store 20 | } 21 | 22 | func NewProductsDomain(s types.Store) *Products { 23 | return &Products{ 24 | store: s, 25 | } 26 | } 27 | 28 | func (d *Products) GetProduct(ctx context.Context, id string) (*types.Product, error) { 29 | product, err := d.store.Get(ctx, id) 30 | if err != nil { 31 | return nil, fmt.Errorf("%w", err) 32 | } 33 | 34 | return product, nil 35 | } 36 | 37 | func (d *Products) AllProducts(ctx context.Context, next *string) (types.ProductRange, error) { 38 | if next != nil && strings.TrimSpace(*next) == "" { 39 | next = nil 40 | } 41 | 42 | productRange, err := d.store.All(ctx, next) 43 | if err != nil { 44 | return productRange, fmt.Errorf("%w", err) 45 | } 46 | 47 | return productRange, nil 48 | } 49 | 50 | func (d *Products) PutProduct(ctx context.Context, id string, body []byte) (*types.Product, error) { 51 | product := types.Product{} 52 | if err := json.Unmarshal(body, &product); err != nil { 53 | return nil, fmt.Errorf("%w", ErrJsonUnmarshal) 54 | } 55 | 56 | if product.Id != id { 57 | return nil, fmt.Errorf("%w", ErrProductIdMismatch) 58 | } 59 | 60 | err := d.store.Put(ctx, product) 61 | if err != nil { 62 | return nil, fmt.Errorf("%w", err) 63 | } 64 | 65 | return &product, nil 66 | } 67 | 68 | func (d *Products) DeleteProduct(ctx context.Context, id string) error { 69 | err := d.store.Delete(ctx, id) 70 | if err != nil { 71 | return fmt.Errorf("%w", err) 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /domain/products_stream.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws-samples/serverless-go-demo/types" 8 | ) 9 | 10 | type ProductsStream struct { 11 | bus types.Bus 12 | } 13 | 14 | func NewProductsStream(b types.Bus) *ProductsStream { 15 | return &ProductsStream{ 16 | bus: b, 17 | } 18 | } 19 | 20 | func (p *ProductsStream) Publish(ctx context.Context, events []types.Event) ([]types.FailedEvent, error) { 21 | failedEvents, err := p.bus.Put(ctx, events) 22 | if err != nil { 23 | return failedEvents, fmt.Errorf("%w", err) 24 | } 25 | 26 | return failedEvents, nil 27 | } 28 | -------------------------------------------------------------------------------- /domain/products_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | // +build unit 3 | 4 | package domain 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | 11 | "github.com/aws-samples/serverless-go-demo/store" 12 | "github.com/aws-samples/serverless-go-demo/types" 13 | "github.com/aws-samples/serverless-go-demo/types/mocks" 14 | "github.com/golang/mock/gomock" 15 | ) 16 | 17 | func TestGetProductNotFound(t *testing.T) { 18 | memoryStore := store.NewMemoryStore() 19 | domain := NewProductsDomain(memoryStore) 20 | 21 | product, err := domain.GetProduct(context.Background(), "1") 22 | if err != nil { 23 | t.Errorf("GetProduct returned an error: %w", err) 24 | } 25 | 26 | if product != nil { 27 | t.Error("GetProduct returned unexpected product") 28 | } 29 | } 30 | 31 | func TestGetExistingProduct(t *testing.T) { 32 | ctx := context.Background() 33 | 34 | memoryStore := store.NewMemoryStore() 35 | memoryStore.Put(ctx, types.Product{ 36 | Id: "iXR", 37 | Name: "iPhone XML", 38 | Price: 0.123, 39 | }) 40 | 41 | domain := NewProductsDomain(memoryStore) 42 | 43 | product, err := domain.GetProduct(context.Background(), "iXR") 44 | if err != nil { 45 | t.Errorf("GetProduct returned an error: %w", err) 46 | } 47 | 48 | if product == nil { 49 | t.Errorf("GetProduct returned nil object") 50 | return 51 | } 52 | 53 | if product.Name != "iPhone XML" { 54 | t.Errorf("GetProduct returned wrong product name") 55 | } 56 | 57 | if product.Price != 0.123 { 58 | t.Errorf("GetProduct returned wrong price") 59 | } 60 | } 61 | 62 | func TestGetInternalStoreError(t *testing.T) { 63 | ctrl := gomock.NewController(t) 64 | ctx := context.Background() 65 | 66 | store := mocks.NewMockStore(ctrl) 67 | store.EXPECT(). 68 | Get(ctx, gomock.Eq("1")). 69 | Return(nil, errors.New("internal error")) 70 | 71 | domain := NewProductsDomain(store) 72 | 73 | product, err := domain.GetProduct(ctx, "1") 74 | if product != nil { 75 | t.Error("Got unexpected product") 76 | } 77 | 78 | if err == nil { 79 | t.Error("Expecting an error to be returned") 80 | return 81 | } 82 | 83 | if err.Error() != "internal error" { 84 | t.Errorf("Got unexpected error: %s", err) 85 | } 86 | } 87 | 88 | func TestAllProductsWithInvalidNext(t *testing.T) { 89 | ctrl := gomock.NewController(t) 90 | ctx := context.Background() 91 | 92 | store := mocks.NewMockStore(ctrl) 93 | store.EXPECT(). 94 | All(ctx, gomock.Nil()). 95 | AnyTimes() 96 | 97 | domain := NewProductsDomain(store) 98 | 99 | t.Parallel() 100 | 101 | t.Run("with nil 'next'", func(t *testing.T) { 102 | domain.AllProducts(ctx, nil) 103 | }) 104 | 105 | t.Run("with empty 'next'", func(t *testing.T) { 106 | next := "" 107 | domain.AllProducts(ctx, &next) 108 | }) 109 | 110 | t.Run("with empty spaces 'next'", func(t *testing.T) { 111 | next := " " 112 | domain.AllProducts(ctx, &next) 113 | }) 114 | } 115 | 116 | func TestAllProductsInternalStoreError(t *testing.T) { 117 | ctrl := gomock.NewController(t) 118 | ctx := context.Background() 119 | 120 | store := mocks.NewMockStore(ctrl) 121 | store.EXPECT(). 122 | All(ctx, gomock.All()). 123 | Return(types.ProductRange{}, errors.New("internal error")) 124 | 125 | domain := NewProductsDomain(store) 126 | 127 | _, err := domain.AllProducts(ctx, nil) 128 | if err == nil { 129 | t.Error("Expecting an error to be returned") 130 | return 131 | } 132 | 133 | if err.Error() != "internal error" { 134 | t.Errorf("Got unexpected error: %s", err) 135 | } 136 | } 137 | 138 | func TestAllProducts(t *testing.T) { 139 | memoryStore := store.NewMemoryStore() 140 | domain := NewProductsDomain(memoryStore) 141 | ctx := context.Background() 142 | 143 | t.Run("with an empty store", func(t *testing.T) { 144 | productRange, err := domain.AllProducts(ctx, nil) 145 | if err != nil { 146 | t.Errorf("Got unexpected error: %w", err) 147 | } 148 | 149 | if len(productRange.Products) != 0 { 150 | t.Errorf("Got unexpected products") 151 | } 152 | }) 153 | 154 | t.Run("with products on the store", func(t *testing.T) { 155 | memoryStore.Put(ctx, types.Product{ 156 | Id: "iXR", 157 | Name: "iPhone XML", 158 | Price: 0.123, 159 | }) 160 | 161 | productRange, err := domain.AllProducts(ctx, nil) 162 | if err != nil { 163 | t.Errorf("Got unexpected error: %w", err) 164 | } 165 | 166 | if len(productRange.Products) != 1 { 167 | t.Errorf("Got unexpected products") 168 | } 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /functions/delete-product/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "", 3 | "resource": "/{id}", 4 | "path": "/1", 5 | "httpMethod": "DELETE", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "id": "1" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "09/Apr/2015:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/prod/1", 118 | "resourcePath": "/{id}", 119 | "httpMethod": "GET", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/delete-product/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/aws-samples/serverless-go-demo/domain" 8 | "github.com/aws-samples/serverless-go-demo/handlers" 9 | "github.com/aws-samples/serverless-go-demo/store" 10 | 11 | "github.com/aws/aws-lambda-go/lambda" 12 | ) 13 | 14 | func main() { 15 | tableName, ok := os.LookupEnv("TABLE") 16 | if !ok { 17 | panic("Need TABLE environment variable") 18 | } 19 | 20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName) 21 | domain := domain.NewProductsDomain(dynamodb) 22 | handler := handlers.NewAPIGatewayV2Handler(domain) 23 | lambda.Start(handler.DeleteHandler) 24 | } 25 | -------------------------------------------------------------------------------- /functions/get-product/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "eyJ0ZXN0IjoiYm9keSJ9", 3 | "resource": "/{id}", 4 | "path": "/1", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "id": "1" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "09/Apr/2015:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/prod/1", 118 | "resourcePath": "/{id}", 119 | "httpMethod": "GET", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/get-product/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/aws-samples/serverless-go-demo/domain" 8 | "github.com/aws-samples/serverless-go-demo/handlers" 9 | "github.com/aws-samples/serverless-go-demo/store" 10 | 11 | "github.com/aws/aws-lambda-go/lambda" 12 | ) 13 | 14 | func main() { 15 | tableName, ok := os.LookupEnv("TABLE") 16 | if !ok { 17 | panic("Need TABLE environment variable") 18 | } 19 | 20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName) 21 | domain := domain.NewProductsDomain(dynamodb) 22 | handler := handlers.NewAPIGatewayV2Handler(domain) 23 | lambda.Start(handler.GetHandler) 24 | } 25 | -------------------------------------------------------------------------------- /functions/get-products/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/aws-samples/serverless-go-demo/domain" 8 | "github.com/aws-samples/serverless-go-demo/handlers" 9 | "github.com/aws-samples/serverless-go-demo/store" 10 | 11 | "github.com/aws/aws-lambda-go/lambda" 12 | ) 13 | 14 | func main() { 15 | tableName, ok := os.LookupEnv("TABLE") 16 | if !ok { 17 | panic("Need TABLE environment variable") 18 | } 19 | 20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName) 21 | domain := domain.NewProductsDomain(dynamodb) 22 | handler := handlers.NewAPIGatewayV2Handler(domain) 23 | lambda.Start(handler.AllHandler) 24 | } 25 | -------------------------------------------------------------------------------- /functions/products-stream/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "1", 5 | "eventVersion": "1.0", 6 | "dynamodb": { 7 | "Keys": { 8 | "Id": { 9 | "N": "101" 10 | } 11 | }, 12 | "NewImage": { 13 | "Message": { 14 | "S": "New item!" 15 | }, 16 | "Id": { 17 | "N": "101" 18 | } 19 | }, 20 | "StreamViewType": "NEW_AND_OLD_IMAGES", 21 | "SequenceNumber": "111", 22 | "SizeBytes": 26 23 | }, 24 | "awsRegion": "us-west-2", 25 | "eventName": "INSERT", 26 | "eventSourceARN": "eventsourceArn", 27 | "eventSource": "aws:dynamodb" 28 | }, 29 | { 30 | "eventID": "2", 31 | "eventVersion": "1.0", 32 | "dynamodb": { 33 | "OldImage": { 34 | "Message": { 35 | "S": "New item!" 36 | }, 37 | "Id": { 38 | "N": "101" 39 | } 40 | }, 41 | "SequenceNumber": "222", 42 | "Keys": { 43 | "Id": { 44 | "N": "101" 45 | } 46 | }, 47 | "SizeBytes": 59, 48 | "NewImage": { 49 | "Message": { 50 | "S": "This item has changed" 51 | }, 52 | "Id": { 53 | "N": "101" 54 | } 55 | }, 56 | "StreamViewType": "NEW_AND_OLD_IMAGES" 57 | }, 58 | "awsRegion": "us-west-2", 59 | "eventName": "MODIFY", 60 | "eventSourceARN": "sourceArn", 61 | "eventSource": "aws:dynamodb" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /functions/products-stream/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/aws/aws-lambda-go/lambda" 8 | 9 | "github.com/aws-samples/serverless-go-demo/bus" 10 | "github.com/aws-samples/serverless-go-demo/domain" 11 | "github.com/aws-samples/serverless-go-demo/handlers" 12 | ) 13 | 14 | func main() { 15 | eventBusName, ok := os.LookupEnv("EVENT_BUS_NAME") 16 | if !ok { 17 | panic("Need EVENT_BUS_NAME environment variable") 18 | } 19 | 20 | store := bus.NewEventBridgeBus(context.TODO(), eventBusName) 21 | domain := domain.NewProductsStream(store) 22 | handler := handlers.NewDynamoDBEventHandler(domain) 23 | lambda.Start(handler.StreamHandler) 24 | } 25 | -------------------------------------------------------------------------------- /functions/put-product/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"id\":\"1\", \"name\":\"product\", \"price\": 0.123}", 3 | "resource": "/{id}", 4 | "path": "/1", 5 | "httpMethod": "PUT", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "multiValueQueryStringParameters": { 11 | "foo": [ 12 | "bar" 13 | ] 14 | }, 15 | "pathParameters": { 16 | "id": "1" 17 | }, 18 | "stageVariables": { 19 | "baz": "qux" 20 | }, 21 | "headers": { 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 23 | "Accept-Encoding": "gzip, deflate, sdch", 24 | "Accept-Language": "en-US,en;q=0.8", 25 | "Cache-Control": "max-age=0", 26 | "CloudFront-Forwarded-Proto": "https", 27 | "CloudFront-Is-Desktop-Viewer": "true", 28 | "CloudFront-Is-Mobile-Viewer": "false", 29 | "CloudFront-Is-SmartTV-Viewer": "false", 30 | "CloudFront-Is-Tablet-Viewer": "false", 31 | "CloudFront-Viewer-Country": "US", 32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 33 | "Upgrade-Insecure-Requests": "1", 34 | "User-Agent": "Custom User Agent String", 35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 38 | "X-Forwarded-Port": "443", 39 | "X-Forwarded-Proto": "https" 40 | }, 41 | "multiValueHeaders": { 42 | "Accept": [ 43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 44 | ], 45 | "Accept-Encoding": [ 46 | "gzip, deflate, sdch" 47 | ], 48 | "Accept-Language": [ 49 | "en-US,en;q=0.8" 50 | ], 51 | "Cache-Control": [ 52 | "max-age=0" 53 | ], 54 | "CloudFront-Forwarded-Proto": [ 55 | "https" 56 | ], 57 | "CloudFront-Is-Desktop-Viewer": [ 58 | "true" 59 | ], 60 | "CloudFront-Is-Mobile-Viewer": [ 61 | "false" 62 | ], 63 | "CloudFront-Is-SmartTV-Viewer": [ 64 | "false" 65 | ], 66 | "CloudFront-Is-Tablet-Viewer": [ 67 | "false" 68 | ], 69 | "CloudFront-Viewer-Country": [ 70 | "US" 71 | ], 72 | "Host": [ 73 | "0123456789.execute-api.us-east-1.amazonaws.com" 74 | ], 75 | "Upgrade-Insecure-Requests": [ 76 | "1" 77 | ], 78 | "User-Agent": [ 79 | "Custom User Agent String" 80 | ], 81 | "Via": [ 82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 83 | ], 84 | "X-Amz-Cf-Id": [ 85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 86 | ], 87 | "X-Forwarded-For": [ 88 | "127.0.0.1, 127.0.0.2" 89 | ], 90 | "X-Forwarded-Port": [ 91 | "443" 92 | ], 93 | "X-Forwarded-Proto": [ 94 | "https" 95 | ] 96 | }, 97 | "requestContext": { 98 | "accountId": "123456789012", 99 | "resourceId": "123456", 100 | "stage": "prod", 101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 102 | "requestTime": "09/Apr/2015:12:34:56 +0000", 103 | "requestTimeEpoch": 1428582896000, 104 | "identity": { 105 | "cognitoIdentityPoolId": null, 106 | "accountId": null, 107 | "cognitoIdentityId": null, 108 | "caller": null, 109 | "accessKey": null, 110 | "sourceIp": "127.0.0.1", 111 | "cognitoAuthenticationType": null, 112 | "cognitoAuthenticationProvider": null, 113 | "userArn": null, 114 | "userAgent": "Custom User Agent String", 115 | "user": null 116 | }, 117 | "path": "/prod/1", 118 | "resourcePath": "/{id}", 119 | "httpMethod": "GET", 120 | "apiId": "1234567890", 121 | "protocol": "HTTP/1.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/put-product/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/aws-samples/serverless-go-demo/domain" 8 | "github.com/aws-samples/serverless-go-demo/handlers" 9 | "github.com/aws-samples/serverless-go-demo/store" 10 | 11 | "github.com/aws/aws-lambda-go/lambda" 12 | ) 13 | 14 | func main() { 15 | tableName, ok := os.LookupEnv("TABLE") 16 | if !ok { 17 | panic("Need TABLE environment variable") 18 | } 19 | 20 | dynamodb := store.NewDynamoDBStore(context.TODO(), tableName) 21 | domain := domain.NewProductsDomain(dynamodb) 22 | handler := handlers.NewAPIGatewayV2Handler(domain) 23 | lambda.Start(handler.PutHandler) 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws-samples/serverless-go-demo 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.27.0 7 | github.com/aws/aws-sdk-go-v2 v1.11.2 8 | github.com/aws/aws-sdk-go-v2/config v1.11.0 9 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4 10 | github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.9.2 11 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0 12 | github.com/golang/mock v1.6.0 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect 17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect 27 | github.com/aws/smithy-go v1.9.0 // indirect 28 | github.com/jmespath/go-jmespath v0.4.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aws/aws-lambda-go v1.27.0 h1:aLzrJwdyHoF1A18YeVdJjX8Ixkd+bpogdxVInvHcWjM= 3 | github.com/aws/aws-lambda-go v1.27.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= 4 | github.com/aws/aws-sdk-go-v2 v1.11.2 h1:SDiCYqxdIYi6HgQfAWRhgdZrdnOuGyLDJVRSWLeHWvs= 5 | github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= 6 | github.com/aws/aws-sdk-go-v2/config v1.11.0 h1:Czlld5zBB61A3/aoegA9/buZulwL9mHHfizh/Oq+Kqs= 7 | github.com/aws/aws-sdk-go-v2/config v1.11.0/go.mod h1:VrQDJGFBM5yZe+IOeenNZ/DWoErdny+k2MHEIpwDsEY= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.6.4 h1:2hvbUoHufns0lDIsaK8FVCMukT1WngtZPavN+W2FkSw= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.6.4/go.mod h1:tTrhvBPHyPde4pdIPSba4Nv7RYr4wP9jxXEDa1bKn/8= 10 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4 h1:9WteVf5jmManG9HlxTFsk1+MT1IZ8S/8rvR+3A3OKng= 11 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4/go.mod h1:MWyvQ5I9fEsoV+Im6IgpILXlAaypjlRqUkyS5GP5pIo= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 h1:KiN5TPOLrEjbGCvdTQR4t0U4T87vVwALZ5Bg3jpMqPY= 13 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2/go.mod h1:dF2F6tXEOgmW5X1ZFO/EPtWrcm7XkW07KNcJUGNtt4s= 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 h1:XJLnluKuUxQG255zPNe+04izXl7GSyUVafIsgfv9aw4= 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2/go.mod h1:SgKKNBIoDC/E1ZCDhhMW3yalWjwuLjMcpLzsM/QQnWo= 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 h1:EauRoYZVNPlidZSZJDscjJBQ22JhVF2+tdteatax2Ak= 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2/go.mod h1:xT4XX6w5Sa3dhg50JrYyy3e4WPYo/+WjY/BXtqXVunU= 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 h1:IQup8Q6lorXeiA/rK72PeToWoWK8h7VAPgHNWdSrtgE= 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2/go.mod h1:VITe/MdW6EMXPb0o0txu/fsonXbMHUU2OC2Qp7ivU4o= 20 | github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.9.2 h1:3KMmqYzUxNPLuOYo6+n6BbqmIV3T5RfeK2NOpgTtkFU= 21 | github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.9.2/go.mod h1:FDL3tQ4lF3/k1xsp4WVNmXgWS/o/v/1uwn+lrAOgolE= 22 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0 h1:jzvWaPf99rIjqEBxh9uGKxtnIykU/SOXY/nfvThhJvI= 23 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0/go.mod h1:ELltfl9ri0n4sZ/VjPZBgemNMd9mYIpCAuZhc7NP7l4= 24 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1 h1:AQurjazY9KPUxvq4EBN9Q3iWGaDrcqfpfSWtkP0Qy+g= 25 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1/go.mod h1:RiesWyLiePOOwyT5ySDupQosvbG+OTMv9pws/EhDu4U= 26 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 h1:lPLbw4Gn59uoKqvOfSnkJr54XWk5Ak1NK20ZEiSWb3U= 27 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0/go.mod h1:80NaCIH9YU3rzTTs/J/ECATjXuRqzo/wB6ukO6MZ0XY= 28 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3 h1:ru9+IpkVIuDvIkm9Q0DEjtWHnh6ITDoZo8fH2dIjlqQ= 29 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3/go.mod h1:zOyLMYyg60yyZpOCniAUuibWVqTU4TuLmMa/Wh4P+HA= 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 h1:CKdUNKmuilw/KNmO2Q53Av8u+ZyXMC2M9aX8Z+c/gzg= 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2/go.mod h1:FgR1tCsn8C6+Hf+N5qkfrE4IXvUL1RgW87sunJ+5J4I= 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 h1:2IDmvSb86KT44lSg1uU4ONpzgWLOuApRl6Tg54mZ6Dk= 33 | github.com/aws/aws-sdk-go-v2/service/sso v1.6.2/go.mod h1:KnIpszaIdwI33tmc/W/GGXyn22c1USYxA/2KyvoeDY0= 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 h1:QKR7wy5e650q70PFKMfGF9sTo0rZgUevSSJ4wxmyWXk= 35 | github.com/aws/aws-sdk-go-v2/service/sts v1.11.1/go.mod h1:UV2N5HaPfdbDpkgkz4sRzWCvQswZjdO1FfqCWl0t7RA= 36 | github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58= 37 | github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 38 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 39 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 44 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 45 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 47 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 49 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 50 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 51 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 55 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 58 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 60 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 61 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 62 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 63 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 67 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 68 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 70 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 88 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 89 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 91 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | -------------------------------------------------------------------------------- /handlers/apigateway.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/aws-samples/serverless-go-demo/domain" 11 | 12 | "github.com/aws/aws-lambda-go/events" 13 | ) 14 | 15 | type APIGatewayV2Handler struct { 16 | products *domain.Products 17 | } 18 | 19 | func NewAPIGatewayV2Handler(d *domain.Products) *APIGatewayV2Handler { 20 | return &APIGatewayV2Handler{ 21 | products: d, 22 | } 23 | } 24 | 25 | func (l *APIGatewayV2Handler) AllHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 26 | next := event.QueryStringParameters["next"] 27 | 28 | productRange, err := l.products.AllProducts(ctx, &next) 29 | if err != nil { 30 | return errResponse(http.StatusInternalServerError, err.Error()), nil 31 | } 32 | 33 | return response(http.StatusOK, productRange), nil 34 | } 35 | 36 | func (l *APIGatewayV2Handler) GetHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 37 | id, ok := event.PathParameters["id"] 38 | if !ok { 39 | return errResponse(http.StatusBadRequest, "missing 'id' parameter in path"), nil 40 | } 41 | 42 | product, err := l.products.GetProduct(ctx, id) 43 | 44 | if err != nil { 45 | return errResponse(http.StatusInternalServerError, err.Error()), nil 46 | } 47 | if product == nil { 48 | return errResponse(http.StatusNotFound, "product not found"), nil 49 | } else { 50 | return response(http.StatusOK, product), nil 51 | } 52 | } 53 | 54 | func (l *APIGatewayV2Handler) PutHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 55 | id, ok := event.PathParameters["id"] 56 | if !ok { 57 | return errResponse(http.StatusBadRequest, "missing 'id' parameter in path"), nil 58 | } 59 | 60 | if strings.TrimSpace(event.Body) == "" { 61 | return errResponse(http.StatusBadRequest, "empty request body"), nil 62 | } 63 | 64 | product, err := l.products.PutProduct(ctx, id, []byte(event.Body)) 65 | if err != nil { 66 | if errors.Is(err, domain.ErrJsonUnmarshal) || errors.Is(err, domain.ErrProductIdMismatch) { 67 | return errResponse(http.StatusBadRequest, err.Error()), nil 68 | } else { 69 | return errResponse(http.StatusInternalServerError, err.Error()), nil 70 | } 71 | } 72 | 73 | return response(http.StatusCreated, product), nil 74 | } 75 | 76 | func (l *APIGatewayV2Handler) DeleteHandler(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 77 | id, ok := event.PathParameters["id"] 78 | if !ok { 79 | return errResponse(http.StatusBadRequest, "missing 'id' parameter in path"), nil 80 | } 81 | 82 | err := l.products.DeleteProduct(ctx, id) 83 | if err != nil { 84 | return errResponse(http.StatusInternalServerError, err.Error()), nil 85 | } 86 | 87 | return response(http.StatusOK, nil), nil 88 | } 89 | 90 | func response(code int, object interface{}) events.APIGatewayV2HTTPResponse { 91 | marshalled, err := json.Marshal(object) 92 | if err != nil { 93 | return errResponse(http.StatusInternalServerError, err.Error()) 94 | } 95 | 96 | return events.APIGatewayV2HTTPResponse{ 97 | StatusCode: code, 98 | Headers: map[string]string{ 99 | "Content-Type": "application/json", 100 | }, 101 | Body: string(marshalled), 102 | IsBase64Encoded: false, 103 | } 104 | } 105 | 106 | func errResponse(status int, body string) events.APIGatewayV2HTTPResponse { 107 | message := map[string]string{ 108 | "message": body, 109 | } 110 | 111 | messageBytes, _ := json.Marshal(&message) 112 | 113 | return events.APIGatewayV2HTTPResponse{ 114 | StatusCode: status, 115 | Headers: map[string]string{ 116 | "Content-Type": "application/json", 117 | }, 118 | Body: string(messageBytes), 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /handlers/dynamodb.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | 8 | "github.com/aws-samples/serverless-go-demo/domain" 9 | "github.com/aws-samples/serverless-go-demo/types" 10 | "github.com/aws/aws-lambda-go/events" 11 | ) 12 | 13 | type DynamoDBEventHandler struct { 14 | productStream *domain.ProductsStream 15 | } 16 | 17 | // Can be deleted when this is merged: https://github.com/aws/aws-lambda-go/pull/410/files 18 | 19 | type StreamsEventResponse struct { 20 | BatchItemFailures []BatchItemFailure `json:"batchItemFailures"` 21 | } 22 | 23 | type BatchItemFailure struct { 24 | ItemIdentifier string `json:"itemIdentifier"` 25 | } 26 | 27 | func NewDynamoDBEventHandler(p *domain.ProductsStream) *DynamoDBEventHandler { 28 | return &DynamoDBEventHandler{ 29 | productStream: p, 30 | } 31 | } 32 | 33 | func (d *DynamoDBEventHandler) StreamHandler(ctx context.Context, event events.DynamoDBEvent) (StreamsEventResponse, error) { 34 | internalEvents := make([]types.Event, len(event.Records)) 35 | for i, ddbEvent := range event.Records { 36 | internalEvents[i] = eventFromDynamoDBRecord(ddbEvent) 37 | } 38 | 39 | failedEvents, err := d.productStream.Publish(ctx, internalEvents) 40 | if err != nil { 41 | log.Fatalf("totally failed to publish: %v", err) 42 | return StreamsEventResponse{}, err 43 | } 44 | 45 | if len(failedEvents) > 0 { 46 | itemFailures := make([]BatchItemFailure, len(failedEvents)) 47 | for i, failedItem := range failedEvents { 48 | itemFailures[i] = BatchItemFailure{ItemIdentifier: failedItem.Resources[0]} 49 | } 50 | 51 | return StreamsEventResponse{BatchItemFailures: itemFailures}, nil 52 | } 53 | 54 | return StreamsEventResponse{}, nil 55 | } 56 | 57 | func eventFromDynamoDBRecord(record events.DynamoDBEventRecord) types.Event { 58 | change, err := json.Marshal(record.Change) 59 | if err != nil { 60 | log.Fatalf("cannot unmarshal dynamodb record change: %s", err) 61 | } 62 | 63 | detailType := "" 64 | switch record.EventName { 65 | case string(events.DynamoDBOperationTypeInsert): 66 | detailType = "ProductCreated" 67 | case string(events.DynamoDBOperationTypeModify): 68 | detailType = "ProductUpdated" 69 | case string(events.DynamoDBOperationTypeRemove): 70 | detailType = "ProductDelected" 71 | } 72 | 73 | return types.Event{ 74 | Source: "serverless-go-demo", 75 | Detail: string(change), 76 | DetailType: detailType, 77 | Resources: []string{record.EventID}, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /imgs/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-go-demo/a07a55b1ac373c5293c0238dc771169bdd6e8e81/imgs/diagram.png -------------------------------------------------------------------------------- /imgs/load-test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-go-demo/a07a55b1ac373c5293c0238dc771169bdd6e8e81/imgs/load-test.jpeg -------------------------------------------------------------------------------- /integration-testing/integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "log" 12 | "math/rand" 13 | "net/http" 14 | "os" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/aws-samples/serverless-go-demo/types" 20 | ) 21 | 22 | func randomString(length int) string { 23 | rand.Seed(time.Now().UnixNano()) 24 | b := make([]byte, length) 25 | rand.Read(b) 26 | return fmt.Sprintf("%x", b)[:length] 27 | } 28 | 29 | func getRandomProduct() types.Product { 30 | return types.Product{ 31 | Id: randomString(3), 32 | Name: randomString(10), 33 | Price: rand.Float64(), 34 | } 35 | } 36 | 37 | var apiUrl string 38 | 39 | func init() { 40 | _apiUrl, ok := os.LookupEnv("API_URL") 41 | if !ok { 42 | panic("Can't find API_URL environment variable") 43 | } 44 | 45 | apiUrl = _apiUrl 46 | } 47 | 48 | func TestFlow(t *testing.T) { 49 | client := &http.Client{} 50 | product := getRandomProduct() 51 | 52 | // Put new product 53 | log.Println("PUT new product") 54 | payload, err := json.Marshal(product) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, product.Id), bytes.NewBuffer(payload)) 60 | if err != nil { 61 | panic(err) 62 | } 63 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 64 | resp, err := client.Do(req) 65 | if err != nil { 66 | panic(err) 67 | } 68 | defer resp.Body.Close() 69 | 70 | if resp.StatusCode != http.StatusCreated { 71 | t.Fatalf("Failed to create product. Got response code %d", resp.StatusCode) 72 | } 73 | 74 | // Get product 75 | log.Println("GET product") 76 | req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", apiUrl, product.Id), nil) 77 | if err != nil { 78 | panic(err) 79 | } 80 | resp, err = client.Do(req) 81 | if err != nil { 82 | panic(err) 83 | } 84 | defer resp.Body.Close() 85 | 86 | if resp.StatusCode != http.StatusOK { 87 | t.Fatalf("Failed to get product. Got response code %d", resp.StatusCode) 88 | } 89 | 90 | apiProduct := types.Product{} 91 | body, _ := io.ReadAll(resp.Body) 92 | json.Unmarshal(body, &apiProduct) 93 | 94 | if apiProduct.Id != product.Id { 95 | t.Fatalf("API product ID is different from our own") 96 | } 97 | 98 | if apiProduct.Name != product.Name { 99 | t.Fatalf("API product Name is different from our own") 100 | } 101 | 102 | if apiProduct.Price != product.Price { 103 | t.Fatalf("API product Price is different from our own") 104 | } 105 | 106 | // Get all products 107 | log.Println("GET all products") 108 | 109 | req, err = http.NewRequest(http.MethodGet, apiUrl, nil) 110 | if err != nil { 111 | panic(err) 112 | } 113 | resp, err = client.Do(req) 114 | if err != nil { 115 | panic(err) 116 | } 117 | defer resp.Body.Close() 118 | 119 | if resp.StatusCode != http.StatusOK { 120 | t.Fatalf("Failed to get all products. Got response code %d", resp.StatusCode) 121 | } 122 | 123 | products := types.ProductRange{} 124 | body, _ = io.ReadAll(resp.Body) 125 | json.Unmarshal(body, &products) 126 | 127 | if len(products.Products) < 1 { 128 | t.Fatalf("Failed to get all products. Only got %d", len(products.Products)) 129 | } 130 | 131 | // Delete product 132 | log.Println("DELETE product") 133 | 134 | req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s", apiUrl, product.Id), nil) 135 | if err != nil { 136 | panic(err) 137 | } 138 | resp, err = client.Do(req) 139 | if err != nil { 140 | panic(err) 141 | } 142 | defer resp.Body.Close() 143 | 144 | if resp.StatusCode != http.StatusOK { 145 | t.Fatalf("Failed to delete product. Got response code %d", resp.StatusCode) 146 | } 147 | 148 | // Get product again 149 | log.Println("GET product again") 150 | req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", apiUrl, product.Id), nil) 151 | if err != nil { 152 | panic(err) 153 | } 154 | resp, err = client.Do(req) 155 | if err != nil { 156 | panic(err) 157 | } 158 | defer resp.Body.Close() 159 | 160 | if resp.StatusCode != http.StatusNotFound { 161 | t.Fatal("Got unexpected product") 162 | } 163 | } 164 | 165 | func TestPutProductWithInvalidId(t *testing.T) { 166 | client := &http.Client{} 167 | 168 | product := getRandomProduct() 169 | product.Id = "invalid id" 170 | 171 | log.Println("PUT new product") 172 | payload, err := json.Marshal(product) 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, "not-the-same-id"), bytes.NewBuffer(payload)) 178 | if err != nil { 179 | panic(err) 180 | } 181 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 182 | resp, err := client.Do(req) 183 | if err != nil { 184 | panic(err) 185 | } 186 | defer resp.Body.Close() 187 | 188 | if resp.StatusCode != http.StatusBadRequest { 189 | t.Fatalf("Should have not created product. Got response code %d", resp.StatusCode) 190 | } 191 | 192 | body, _ := io.ReadAll(resp.Body) 193 | if !strings.Contains(string(body), "product ID in path does not match product ID in body") { 194 | t.Fatalf("Wrong body content: %s", string(body)) 195 | } 196 | } 197 | 198 | func TestProductEmpty(t *testing.T) { 199 | client := &http.Client{} 200 | 201 | log.Println("PUT new product") 202 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, "empty-id"), nil) 203 | if err != nil { 204 | panic(err) 205 | } 206 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 207 | resp, err := client.Do(req) 208 | if err != nil { 209 | panic(err) 210 | } 211 | defer resp.Body.Close() 212 | 213 | if resp.StatusCode != http.StatusBadRequest { 214 | t.Fatalf("Should have not created product. Got response code %d", resp.StatusCode) 215 | } 216 | 217 | body, _ := io.ReadAll(resp.Body) 218 | if !strings.Contains(string(body), "empty request body") { 219 | t.Fatalf("Wrong body content: %s", string(body)) 220 | } 221 | } 222 | 223 | func TestPutProductInvalidBody(t *testing.T) { 224 | client := &http.Client{} 225 | 226 | product := getRandomProduct() 227 | 228 | log.Println("PUT new product") 229 | req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", apiUrl, product.Id), bytes.NewReader([]byte("invalid body"))) 230 | if err != nil { 231 | panic(err) 232 | } 233 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 234 | resp, err := client.Do(req) 235 | if err != nil { 236 | panic(err) 237 | } 238 | defer resp.Body.Close() 239 | 240 | if resp.StatusCode != http.StatusBadRequest { 241 | t.Fatalf("Should have not created product. Got response code %d", resp.StatusCode) 242 | } 243 | 244 | body, _ := io.ReadAll(resp.Body) 245 | if !strings.Contains(string(body), "failed to parse product from request body") { 246 | t.Fatalf("Wrong body content: %s", string(body)) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /load-testing/generator.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const COLORS = [ 4 | "Red", "Green", "Blue", "Yellow", "Orange", "Purple", "Pink", "Brown", 5 | "Black", "White", "Gray", "Silver", "Gold", "Cyan", "Magenta", "Maroon", 6 | "Navy", "Olive", "Teal", "Aqua", "Lime", "Coral", "Aquamarine", 7 | "Turquoise", "Violet", "Indigo", "Plum", "Crimson", "Salmon", "Coral", 8 | "Khaki", "Beige", 9 | ]; 10 | 11 | const PRODUCTS = [ 12 | "Shoes", "Sweatshirts", "Hats", "Pants", "Shirts", "T-Shirts", "Trousers", 13 | "Jackets", "Shorts", "Skirts", "Dresses", "Coats", "Jeans", "Blazers", 14 | "Socks", "Gloves", "Belts", "Bags", "Shoes", "Sunglasses", "Watches", 15 | "Jewelry", "Ties", "Hair Accessories", "Makeup", "Accessories", 16 | ]; 17 | 18 | module.exports = { 19 | generateProduct: function(context, events, done) { 20 | const color = COLORS[Math.floor(Math.random() * COLORS.length)]; 21 | const name = PRODUCTS[Math.floor(Math.random() * PRODUCTS.length)]; 22 | 23 | context.vars.id = crypto.randomUUID(); 24 | context.vars.name = `${color} ${name}`; 25 | context.vars.price = Math.round(Math.random() * 10000) / 100; 26 | 27 | return done(); 28 | }, 29 | }; -------------------------------------------------------------------------------- /load-testing/load-test.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "{{ $processEnvironment.API_URL }}" 3 | processor: generator.js 4 | phases: 5 | - duration: 600 6 | arrivalRate: 300 7 | 8 | scenarios: 9 | - name: "Generate products" 10 | weight: 8 11 | flow: 12 | - function: generateProduct 13 | - put: 14 | url: "/{{ id }}" 15 | headers: 16 | Content-Type: "application/json" 17 | json: 18 | id: "{{ id }}" 19 | name: "{{ name }}" 20 | price: "{{ price }}" 21 | - get: 22 | url: "/{{ id }}" 23 | - think: 3 24 | - delete: 25 | url: "/{{ id }}" 26 | - name: "Get products" 27 | weight: 2 28 | flow: 29 | - get: 30 | url: "/" 31 | -------------------------------------------------------------------------------- /store/dynamodb.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 11 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 12 | ddbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" 13 | 14 | "github.com/aws-samples/serverless-go-demo/types" 15 | ) 16 | 17 | type DynamoDBStore struct { 18 | client *dynamodb.Client 19 | tableName string 20 | } 21 | 22 | var _ types.Store = (*DynamoDBStore)(nil) 23 | 24 | func NewDynamoDBStore(ctx context.Context, tableName string) *DynamoDBStore { 25 | cfg, err := config.LoadDefaultConfig(ctx) 26 | if err != nil { 27 | log.Fatalf("unable to load SDK config, %v", err) 28 | } 29 | 30 | client := dynamodb.NewFromConfig(cfg) 31 | 32 | return &DynamoDBStore{ 33 | client: client, 34 | tableName: tableName, 35 | } 36 | } 37 | 38 | func (d *DynamoDBStore) All(ctx context.Context, next *string) (types.ProductRange, error) { 39 | productRange := types.ProductRange{ 40 | Products: []types.Product{}, 41 | } 42 | 43 | input := &dynamodb.ScanInput{ 44 | TableName: &d.tableName, 45 | Limit: aws.Int32(20), 46 | } 47 | 48 | if next != nil { 49 | input.ExclusiveStartKey = map[string]ddbtypes.AttributeValue{ 50 | "id": &ddbtypes.AttributeValueMemberS{Value: *next}, 51 | } 52 | } 53 | 54 | result, err := d.client.Scan(ctx, input) 55 | 56 | if err != nil { 57 | return productRange, fmt.Errorf("failed to get items from DynamoDB: %w", err) 58 | } 59 | 60 | err = attributevalue.UnmarshalListOfMaps(result.Items, &productRange.Products) 61 | if err != nil { 62 | return productRange, fmt.Errorf("failed to unmarshal data from DynamoDB: %w", err) 63 | } 64 | 65 | if len(result.LastEvaluatedKey) > 0 { 66 | if key, ok := result.LastEvaluatedKey["id"]; ok { 67 | nextKey := key.(*ddbtypes.AttributeValueMemberS).Value 68 | productRange.Next = &nextKey 69 | } 70 | } 71 | 72 | return productRange, nil 73 | } 74 | 75 | func (d *DynamoDBStore) Get(ctx context.Context, id string) (*types.Product, error) { 76 | response, err := d.client.GetItem(ctx, &dynamodb.GetItemInput{ 77 | TableName: &d.tableName, 78 | Key: map[string]ddbtypes.AttributeValue{ 79 | "id": &ddbtypes.AttributeValueMemberS{Value: id}, 80 | }, 81 | }) 82 | 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to get item from DynamoDB: %w", err) 85 | } 86 | 87 | if len(response.Item) == 0 { 88 | return nil, nil 89 | } 90 | 91 | product := types.Product{} 92 | err = attributevalue.UnmarshalMap(response.Item, &product) 93 | 94 | if err != nil { 95 | return nil, fmt.Errorf("error getting item %w", err) 96 | } 97 | 98 | return &product, nil 99 | } 100 | 101 | func (d *DynamoDBStore) Put(ctx context.Context, product types.Product) error { 102 | item, err := attributevalue.MarshalMap(&product) 103 | if err != nil { 104 | return fmt.Errorf("unable to marshal product: %w", err) 105 | } 106 | 107 | _, err = d.client.PutItem(ctx, &dynamodb.PutItemInput{ 108 | TableName: &d.tableName, 109 | Item: item, 110 | }) 111 | 112 | if err != nil { 113 | return fmt.Errorf("cannot put item: %w", err) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (d *DynamoDBStore) Delete(ctx context.Context, id string) error { 120 | _, err := d.client.DeleteItem(ctx, &dynamodb.DeleteItemInput{ 121 | TableName: &d.tableName, 122 | Key: map[string]ddbtypes.AttributeValue{ 123 | "id": &ddbtypes.AttributeValueMemberS{Value: id}, 124 | }, 125 | }) 126 | 127 | if err != nil { 128 | return fmt.Errorf("can't delete item: %w", err) 129 | } 130 | 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /store/memory.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | 8 | "github.com/aws-samples/serverless-go-demo/types" 9 | ) 10 | 11 | type MemoryStore struct { 12 | storage map[string]types.Product 13 | } 14 | 15 | // Just to make sure MemoryStore implements the Store interface 16 | var _ types.Store = (*MemoryStore)(nil) 17 | 18 | func NewMemoryStore() *MemoryStore { 19 | return &MemoryStore{ 20 | storage: make(map[string]types.Product), 21 | } 22 | } 23 | 24 | func (m *MemoryStore) All(ctx context.Context, next *string) (types.ProductRange, error) { 25 | productRange := types.ProductRange{ 26 | Products: []types.Product{}, 27 | Next: aws.String("random next string"), 28 | } 29 | 30 | for _, v := range m.storage { 31 | productRange.Products = append(productRange.Products, v) 32 | } 33 | 34 | return productRange, nil 35 | } 36 | 37 | func (m *MemoryStore) Get(ctx context.Context, id string) (*types.Product, error) { 38 | p, ok := m.storage[id] 39 | if !ok { 40 | return nil, nil 41 | } 42 | 43 | return &p, nil 44 | } 45 | 46 | func (m *MemoryStore) Put(ctx context.Context, p types.Product) error { 47 | m.storage[p.Id] = p 48 | 49 | return nil 50 | } 51 | 52 | func (m *MemoryStore) Delete(ctx context.Context, id string) error { 53 | delete(m.storage, id) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Globals: 5 | Function: 6 | MemorySize: 128 7 | Architectures: ["arm64"] 8 | Handler: bootstrap 9 | Runtime: provided.al2 10 | Timeout: 5 11 | Tracing: Active 12 | Environment: 13 | Variables: 14 | TABLE: !Ref Table 15 | 16 | Resources: 17 | GetProductsFunction: 18 | Type: AWS::Serverless::Function 19 | Properties: 20 | CodeUri: functions/get-products/ 21 | Events: 22 | Api: 23 | Type: HttpApi 24 | Properties: 25 | Path: / 26 | Method: GET 27 | Policies: 28 | - Version: "2012-10-17" 29 | Statement: 30 | - Effect: Allow 31 | Action: dynamodb:Scan 32 | Resource: !GetAtt Table.Arn 33 | Metadata: 34 | BuildMethod: makefile 35 | 36 | GetProductFunction: 37 | Type: AWS::Serverless::Function 38 | Properties: 39 | CodeUri: functions/get-product/ 40 | Events: 41 | Api: 42 | Type: HttpApi 43 | Properties: 44 | Path: /{id} 45 | Method: GET 46 | Policies: 47 | - Version: "2012-10-17" 48 | Statement: 49 | - Effect: Allow 50 | Action: dynamodb:GetItem 51 | Resource: !GetAtt Table.Arn 52 | Metadata: 53 | BuildMethod: makefile 54 | 55 | DeleteProductFunction: 56 | Type: AWS::Serverless::Function 57 | Properties: 58 | CodeUri: functions/delete-product/ 59 | Events: 60 | Api: 61 | Type: HttpApi 62 | Properties: 63 | Path: /{id} 64 | Method: DELETE 65 | Policies: 66 | - Version: "2012-10-17" 67 | Statement: 68 | - Effect: Allow 69 | Action: dynamodb:DeleteItem 70 | Resource: !GetAtt Table.Arn 71 | Metadata: 72 | BuildMethod: makefile 73 | 74 | PutProductFunction: 75 | Type: AWS::Serverless::Function 76 | Properties: 77 | CodeUri: functions/put-product/ 78 | Events: 79 | Api: 80 | Type: HttpApi 81 | Properties: 82 | Path: /{id} 83 | Method: PUT 84 | Policies: 85 | - Version: "2012-10-17" 86 | Statement: 87 | - Effect: Allow 88 | Action: dynamodb:PutItem 89 | Resource: !GetAtt Table.Arn 90 | Metadata: 91 | BuildMethod: makefile 92 | 93 | DDBStreamsFunction: 94 | Type: AWS::Serverless::Function 95 | Properties: 96 | CodeUri: functions/products-stream/ 97 | Timeout: 10 98 | Events: 99 | TableStream: 100 | Type: DynamoDB 101 | Properties: 102 | BatchSize: 1000 103 | FunctionResponseTypes: 104 | - ReportBatchItemFailures 105 | MaximumBatchingWindowInSeconds: 10 106 | StartingPosition: TRIM_HORIZON 107 | Stream: !GetAtt Table.StreamArn 108 | Environment: 109 | Variables: 110 | EVENT_BUS_NAME: !Ref EventBus 111 | MemorySize: 128 112 | Policies: 113 | - Version: "2012-10-17" 114 | Statement: 115 | - Effect: Allow 116 | Action: events:PutEvents 117 | Resource: !GetAtt EventBus.Arn 118 | 119 | Table: 120 | Type: AWS::DynamoDB::Table 121 | Properties: 122 | AttributeDefinitions: 123 | - AttributeName: id 124 | AttributeType: S 125 | BillingMode: PAY_PER_REQUEST 126 | KeySchema: 127 | - AttributeName: id 128 | KeyType: HASH 129 | StreamSpecification: 130 | StreamViewType: NEW_AND_OLD_IMAGES 131 | 132 | EventBus: 133 | Type: AWS::Events::EventBus 134 | Properties: 135 | Name: !Ref AWS::StackName 136 | 137 | Outputs: 138 | ApiUrl: 139 | Description: "API Gateway endpoint URL" 140 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/" 141 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws-samples/serverless-go-demo/tools 2 | 3 | go 1.17 4 | 5 | require honnef.co/go/tools v0.2.2 6 | 7 | require ( 8 | github.com/BurntSushi/toml v0.3.1 // indirect 9 | golang.org/x/mod v0.3.0 // indirect 10 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect 11 | golang.org/x/tools v0.1.0 // indirect 12 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 6 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 7 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 8 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 9 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 12 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= 18 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 20 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 21 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 22 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 23 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= 24 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 25 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 26 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 27 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 28 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 29 | honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk= 30 | honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= 31 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | // Tools we use during development. 8 | _ "honnef.co/go/tools/cmd/staticcheck" 9 | ) 10 | -------------------------------------------------------------------------------- /types/bus.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "context" 4 | 5 | type Event struct { 6 | Source string 7 | Detail string 8 | DetailType string 9 | Resources []string 10 | } 11 | 12 | type FailedEvent struct { 13 | Event 14 | FailureCode string 15 | FailureMessage string 16 | } 17 | 18 | type Bus interface { 19 | Put(context.Context, []Event) ([]FailedEvent, error) 20 | } 21 | -------------------------------------------------------------------------------- /types/mocks/mock_store.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/aws-samples/serverless-go-demo/types (interfaces: Store) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | types "github.com/aws-samples/serverless-go-demo/types" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockStore is a mock of Store interface. 16 | type MockStore struct { 17 | ctrl *gomock.Controller 18 | recorder *MockStoreMockRecorder 19 | } 20 | 21 | // MockStoreMockRecorder is the mock recorder for MockStore. 22 | type MockStoreMockRecorder struct { 23 | mock *MockStore 24 | } 25 | 26 | // NewMockStore creates a new mock instance. 27 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 28 | mock := &MockStore{ctrl: ctrl} 29 | mock.recorder = &MockStoreMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockStore) EXPECT() *MockStoreMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // All mocks base method. 39 | func (m *MockStore) All(arg0 context.Context, arg1 *string) (types.ProductRange, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "All", arg0, arg1) 42 | ret0, _ := ret[0].(types.ProductRange) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // All indicates an expected call of All. 48 | func (mr *MockStoreMockRecorder) All(arg0, arg1 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockStore)(nil).All), arg0, arg1) 51 | } 52 | 53 | // Delete mocks base method. 54 | func (m *MockStore) Delete(arg0 context.Context, arg1 string) error { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "Delete", arg0, arg1) 57 | ret0, _ := ret[0].(error) 58 | return ret0 59 | } 60 | 61 | // Delete indicates an expected call of Delete. 62 | func (mr *MockStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore)(nil).Delete), arg0, arg1) 65 | } 66 | 67 | // Get mocks base method. 68 | func (m *MockStore) Get(arg0 context.Context, arg1 string) (*types.Product, error) { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 71 | ret0, _ := ret[0].(*types.Product) 72 | ret1, _ := ret[1].(error) 73 | return ret0, ret1 74 | } 75 | 76 | // Get indicates an expected call of Get. 77 | func (mr *MockStoreMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore)(nil).Get), arg0, arg1) 80 | } 81 | 82 | // Put mocks base method. 83 | func (m *MockStore) Put(arg0 context.Context, arg1 types.Product) error { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "Put", arg0, arg1) 86 | ret0, _ := ret[0].(error) 87 | return ret0 88 | } 89 | 90 | // Put indicates an expected call of Put. 91 | func (mr *MockStoreMockRecorder) Put(arg0, arg1 interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockStore)(nil).Put), arg0, arg1) 94 | } 95 | -------------------------------------------------------------------------------- /types/product.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Product struct { 4 | Id string `dynamodbav:"id" json:"id"` 5 | Name string `dynamodbav:"name" json:"name"` 6 | Price float64 `dynamodbav:"price" json:"price"` 7 | } 8 | 9 | type ProductRange struct { 10 | Products []Product `json:"products"` 11 | Next *string `json:"next,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /types/store.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | //go:generate mockgen -destination=./mocks/mock_store.go -package=mocks github.com/aws-samples/serverless-go-demo/types Store 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type Store interface { 10 | All(context.Context, *string) (ProductRange, error) 11 | Get(context.Context, string) (*Product, error) 12 | Put(context.Context, Product) error 13 | Delete(context.Context, string) error 14 | } 15 | --------------------------------------------------------------------------------