05/04/2021 - AWS, GO
Bu, Golang'da yazılmış basit bir AWS DynamoDB CRUD örneğidir. Unutmayın ki bazı dosyaların iyileştirilmesi gerekir. Bu yazıyı olabildiğince kısa tutmak için sabit değerler, tekrarlanmış kod vb. şeyler yaptım. İsteğinize göre yeniden düzenlemekten çekinmeyin.
├── internal
│ ├── domain
│ │ └── error.go
│ ├── pkg
│ │ └── storage
│ │ ├── aws
│ │ │ ├── aws.go
│ │ │ └── user_storage.go
│ │ └── user_storer.go
│ └── user
│ ├── controller.go
│ └── models.go
├── main.go
└── migrations
└── users.json
Öncelikle bölüm anahtarı (partition key) olarak uuid
alanını kullanan users
tablosunu oluşturmanız gerekir.
$ aws --profile localstack --endpoint-url http://localhost:4566 dynamodb create-table --cli-input-json file://migrations/users.json
# migrations/users.json
{
"TableName": "users",
"AttributeDefinitions": [
{
"AttributeName": "uuid",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "uuid",
"KeyType": "HASH"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
}
Adreslere fazla kafa yormayın çünkü sonlarında aşağıdaki gibi son ekler olmamalı!
package main
import (
"log"
"net/http"
"time"
"github.com/you/aws/internal/pkg/storage/aws"
"github.com/you/aws/internal/user"
)
func main() {
// Create a session instance.
ses, err := aws.New(aws.Config{
Address: "http://localhost:4566",
Region: "eu-west-1",
Profile: "localstack",
ID: "test",
Secret: "test",
})
if err != nil {
log.Fatalln(err)
}
// Instantiate HTTP app
usr := user.Controller{
Storage: aws.NewUserStorage(ses, time.Second*5),
}
// Instantiate HTTP router
rtr := http.NewServeMux()
rtr.HandleFunc("/api/v1/users/create", usr.Create)
rtr.HandleFunc("/api/v1/users/find", usr.Find)
rtr.HandleFunc("/api/v1/users/delete", usr.Delete)
rtr.HandleFunc("/api/v1/users/update", usr.Update)
// Start HTTP server
log.Fatalln(http.ListenAndServe(":8080", rtr))
}
package user
import (
"encoding/json"
"log"
"net/http"
"github.com/you/aws/internal/domain"
"github.com/you/aws/internal/pkg/storage"
"github.com/google/uuid"
)
type Controller struct {
Storage storage.UserStorer
}
// POST /api/v1/users/create
func (c Controller) Create(w http.ResponseWriter, r *http.Request) {
var req User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
id := uuid.New().String()
err := c.Storage.Insert(r.Context(), storage.User{
UUID: id,
Name: req.Name,
Level: req.Level,
IsBlocked: req.IsBlocked,
CreatedAt: req.CreatedAt,
Roles: req.Roles,
})
if err != nil {
switch err {
case domain.ErrConflict:
w.WriteHeader(http.StatusConflict)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(id))
}
// GET /api/v1/users/find?id={UUID}
func (c Controller) Find(w http.ResponseWriter, r *http.Request) {
res, err := c.Storage.Find(r.Context(), r.URL.Query().Get("id"))
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}
user := User{
UUID: res.UUID,
Name: res.Name,
Level: res.Level,
IsBlocked: res.IsBlocked,
CreatedAt: res.CreatedAt,
Roles: res.Roles,
}
data, err := json.Marshal(user)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(data)
}
// DELETE /api/v1/users/delete?id={UUID}
func (c Controller) Delete(w http.ResponseWriter, r *http.Request) {
err := c.Storage.Delete(r.Context(), r.URL.Query().Get("id"))
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
// PATCH /api/v1/users/update?id={UUID}
func (c Controller) Update(w http.ResponseWriter, r *http.Request) {
var req User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err := c.Storage.Update(r.Context(), storage.User{
UUID: r.URL.Query().Get("id"),
Name: req.Name,
Level: req.Level,
Roles: req.Roles,
})
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusConflict)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
package user
import "time"
type User struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Level int `json:"level"`
IsBlocked bool `json:"is_blocked"`
CreatedAt time.Time `json:"created_at"`
Roles []string `json:"roles"`
}
package domain
import "errors"
var (
ErrInternal = errors.New("internal")
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)
package storage
import (
"context"
"time"
)
type User struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Level int `json:"level"`
IsBlocked bool `json:"is_blocked"`
CreatedAt time.Time `json:"created_at"`
Roles []string `json:"roles"`
}
type UserStorer interface {
Insert(ctx context.Context, user User) error
Find(ctx context.Context, uuid string) (User, error)
Delete(ctx context.Context, uuid string) error
Update(ctx context.Context, user User) error
}
package aws
import (
"context"
"fmt"
"log"
"time"
"github.com/you/aws/internal/domain"
"github.com/you/aws/internal/pkg/storage"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
var _ storage.UserStorer = UserStorage{}
type UserStorage struct {
timeout time.Duration
client *dynamodb.DynamoDB
}
func NewUserStorage(session *session.Session, timeout time.Duration) UserStorage {
return UserStorage{
timeout: timeout,
client: dynamodb.New(session),
}
}
func (u UserStorage) Insert(ctx context.Context, user storage.User) error {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()
item, err := dynamodbattribute.MarshalMap(user)
if err != nil {
log.Println(err)
return domain.ErrInternal
}
input := &dynamodb.PutItemInput{
TableName: aws.String("users"),
Item: item,
ExpressionAttributeNames: map[string]*string{
"#uuid": aws.String("uuid"),
},
ConditionExpression: aws.String("attribute_not_exists(#uuid)"),
}
if _, err := u.client.PutItemWithContext(ctx, input); err != nil {
log.Println(err)
if _, ok := err.(*dynamodb.ConditionalCheckFailedException); ok {
return domain.ErrConflict
}
return domain.ErrInternal
}
return nil
}
func (u UserStorage) Find(ctx context.Context, uuid string) (storage.User, error) {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()
input := &dynamodb.GetItemInput{
TableName: aws.String("users"),
Key: map[string]*dynamodb.AttributeValue{
"uuid": {S: aws.String(uuid)},
},
}
res, err := u.client.GetItemWithContext(ctx, input)
if err != nil {
log.Println(err)
return storage.User{}, domain.ErrInternal
}
if res.Item == nil {
return storage.User{}, domain.ErrNotFound
}
var user storage.User
if err := dynamodbattribute.UnmarshalMap(res.Item, &user); err != nil {
log.Println(err)
return storage.User{}, domain.ErrInternal
}
return user, nil
}
func (u UserStorage) Delete(ctx context.Context, uuid string) error {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()
input := &dynamodb.DeleteItemInput{
TableName: aws.String("users"),
Key: map[string]*dynamodb.AttributeValue{
"uuid": {S: aws.String(uuid)},
},
}
if _, err := u.client.DeleteItemWithContext(ctx, input); err != nil {
log.Println(err)
return domain.ErrInternal
}
return nil
}
func (u UserStorage) Update(ctx context.Context, user storage.User) error {
ctx, cancel := context.WithTimeout(ctx, u.timeout)
defer cancel()
roles := make([]*dynamodb.AttributeValue, len(user.Roles))
for i, role := range user.Roles {
roles[i] = &dynamodb.AttributeValue{S: aws.String(role)}
}
input := &dynamodb.UpdateItemInput{
TableName: aws.String("users"),
Key: map[string]*dynamodb.AttributeValue{
"uuid": {S: aws.String(user.UUID)},
},
ExpressionAttributeNames: map[string]*string{
"#name": aws.String("name"),
"#level": aws.String("level"),
"#roles": aws.String("roles"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":name": {S: aws.String(user.Name)},
":level": {N: aws.String(fmt.Sprint(user.Level))},
":roles": {L: roles},
},
UpdateExpression: aws.String("set #name = :name, #level = :level, #roles = :roles"),
ReturnValues: aws.String("UPDATED_NEW"),
}
if _, err := u.client.UpdateItemWithContext(ctx, input); err != nil {
log.Println(err)
return domain.ErrInternal
}
return nil
}
package aws
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
)
type Config struct {
Address string
Region string
Profile string
ID string
Secret string
}
func New(config Config) (*session.Session, error) {
return session.NewSessionWithOptions(
session.Options{
Config: aws.Config{
Credentials: credentials.NewStaticCredentials(config.ID, config.Secret, ""),
Region: aws.String(config.Region),
Endpoint: aws.String(config.Address),
S3ForcePathStyle: aws.Bool(true),
},
Profile: config.Profile,
},
)
}
# Create
curl --location --request POST 'http://localhost:8080/api/v1/users/create' \
--data-raw '{
"name": "inanzzz",
"level": 3,
"is_blocked": false,
"created_at": "2020-01-31T23:59:00Z",
"roles": ["accounts", "admin"]
}'
# Find
curl --location --request GET 'http://localhost:8080/api/v1/users/find?id=80638f40-d248-49be-90ce-88d5b1b4ecd4'
# Delete
curl --location --request DELETE 'http://localhost:8080/api/v1/users/delete?id=80638f40-d248-49be-90ce-88d5b1b4ecd4'
# Update
curl --location --request PATCH 'http://localhost:8080/api/v1/users/update?id=347ac592-b024-4001-9d1b-925abe10c236' \
--data-raw '{
"name": "inanzzz",
"level": 1,
"roles": ["accounts", "admin"]
}'