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.


Yapı


├── 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

Önkoşullar


Ö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
}
}

Dosyalar


main.go


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))
}

controller.go


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)
}

models.go


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"`
}

error.go


package domain

import "errors"

var (
ErrInternal = errors.New("internal")
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)

user_storer.go


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
}

user_storage.go


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
}

aws.go


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,
},
)
}

Test


# 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"]
}'

Referanslar