23/09/2023 - AWS, GO
In this example we are going to simulate e-commerce website and use DynamoDB as database. Along with a simple table, we will be using GSIs for finding or listing items.
Heads up, most of the files here are pretty rough so it is up to you to improve them.
I purposely prefixed all attributes after model to avoid GSIs pulling irrelevant items from the base table.
- Customer
- customer_id
- customer_first_name
- customer_last_name
- Product
- product_id
- product_name
- product_price
- product_tax
- Order
- order_id
- order_total
- order_created_at
- order_customer_id
- OrderProduct
- order_product_quantity
- order_product_total
- order_product_order_id
- order_product_product_id
# Ecommerce
TYPE PART KEY SORT KEY
---- -------- --------
Customer ---- customer customer_id#<customer.id> customer_id#<customer.id>
- Find one (by id) - GetItem(PK = customer_id#<customer.id>, SK = customer_id#<customer.id>)
Product ---- product product_id#<product.id> product_id#<product.id>
- Find one (by id) - GetItem(PK = product_id#<product.id>, SK = product_id#<product.id>)
Order ---- order order_id#<order.id> order_id#<order.id>
- Find one (by id) - GetItem(PK = order_id#<order.id>, SK = order_id#<order.id>)
OrderProduct ---- order_product order_id#<order.id> product_id#<product.id>
- List products (by order id) - Query(PK = order_id#<order.id>, SK begins_with "product_id#")
# CustomersByName
TYPE PART KEY SORT KEY
---- -------- --------
Customer ---- customer customer_first_name customer_last_name
- List customers (by customer name) - Query(PK = customer_first_name)
# OrdersByCustomer
TYPE PART KEY SORT KEY
---- -------- --------
Order ---- order order_customer_id order_created_at
- List orders (by customer id) - Query(PK = order_customer_id)
# OrdersByProduct
TYPE PART KEY
---- --------
OrderProduct ---- order_product order_product_product_id
- List orders (by product id) - Query(PK = order_product_product_id)
├── main.go
├── src
│ ├── api
│ │ ├── customer.go
│ │ ├── order.go
│ │ └── product.go
│ ├── app
│ │ └── error.go
│ ├── model
│ │ ├── api
│ │ │ ├── customer.go
│ │ │ ├── order.go
│ │ │ └── product.go
│ │ └── storage
│ │ └── database
│ │ ├── customer.go
│ │ ├── item.go
│ │ ├── order.go
│ │ └── product.go
│ └── storage
│ └── dynamodb.go
└── table.json
{
"TableName": "Ecommerce",
"AttributeDefinitions": [
{
"AttributeName": "_item_part_key",
"AttributeType": "S"
},
{
"AttributeName": "_item_sort_key",
"AttributeType": "S"
},
{
"AttributeName": "customer_first_name",
"AttributeType": "S"
},
{
"AttributeName": "customer_last_name",
"AttributeType": "S"
},
{
"AttributeName": "order_customer_id",
"AttributeType": "S"
},
{
"AttributeName": "order_created_at",
"AttributeType": "S"
},
{
"AttributeName": "order_product_product_id",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "_item_part_key",
"KeyType": "HASH"
},
{
"AttributeName": "_item_sort_key",
"KeyType": "RANGE"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
},
"GlobalSecondaryIndexes": [
{
"IndexName": "CustomersByName",
"KeySchema": [
{
"AttributeName": "customer_first_name",
"KeyType": "HASH"
},
{
"AttributeName": "customer_last_name",
"KeyType": "RANGE"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
},
{
"IndexName": "OrdersByCustomer",
"KeySchema": [
{
"AttributeName": "order_customer_id",
"KeyType": "HASH"
},
{
"AttributeName": "order_created_at",
"KeyType": "RANGE"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
},
{
"IndexName": "OrdersByProduct",
"KeySchema": [
{
"AttributeName": "order_product_product_id",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
}
]
}
run:
go run -race ecommerce/main.go
db-del:
aws --profile localstack --endpoint-url http://localhost:4566 dynamodb delete-table --table-name Ecommerce > /dev/null
db-crt:
aws --profile localstack --endpoint-url http://localhost:4566 dynamodb create-table --cli-input-json file:///Users/myself/dev/golang/mix/dynamodb/ecommerce/table.json > /dev/null
package main
import (
"context"
"log"
"net/http"
"dynamodb/ecommerce/src/api"
"dynamodb/ecommerce/src/storage"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
func main() {
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
log.Fatalln(err)
}
dyb := dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
o.BaseEndpoint = aws.String("http://localhost:4566")
})
str := storage.DynamoDB{
Client: dyb,
Table: "Ecommerce",
CustomersByNameGSI: "CustomersByName",
OrdersByCustomerGSI: "OrdersByCustomer",
OrdersByProductGSI: "OrdersByProduct",
}
customer := api.Customer{Storage: str}
product := api.Product{Storage: str}
order := api.Order{Storage: str}
rtr := http.NewServeMux()
rtr.HandleFunc("POST /api/v1/customers", customer.Create)
rtr.HandleFunc("GET /api/v1/customers/{customer_id}", customer.FindByID)
rtr.HandleFunc("GET /api/v1/customers", customer.ListCustomersByFirstName)
rtr.HandleFunc("POST /api/v1/products", product.Create)
rtr.HandleFunc("GET /api/v1/products/{product_id}", product.FindByID)
rtr.HandleFunc("POST /api/v1/orders", order.Create)
rtr.HandleFunc("GET /api/v1/orders/{order_id}", order.FindByID)
rtr.HandleFunc("GET /api/v1/orders/{order_id}/products", order.ListProductsByOrderID)
rtr.HandleFunc("GET /api/v1/orders/{customer_id}/customer", order.ListOrdersByCustomerID)
rtr.HandleFunc("GET /api/v1/orders/{product_id}/product", order.ListOrdersByProductID)
http.ListenAndServe(":1234", rtr)
}
package app
import (
"errors"
)
var (
ErrInternal = errors.New("internal")
ErrResourceNotFound = errors.New("resource not found")
)
package api
import (
"encoding/json"
"net/http"
"strconv"
"dynamodb/ecommerce/src/model/api"
"dynamodb/ecommerce/src/model/storage/database"
"dynamodb/ecommerce/src/storage"
"github.com/google/uuid"
)
type Customer struct {
Storage storage.DynamoDB
}
func (c Customer) Create(w http.ResponseWriter, r *http.Request) {
var req api.CreateCustomer
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
model := database.Customer{
ID: uuid.NewString(),
FirstName: req.FirstName,
LastName: req.LastName,
}
model.Setup()
if err := c.Storage.CreateCustomer(r.Context(), model); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(model.ID))
}
func (c Customer) FindByID(w http.ResponseWriter, r *http.Request) {
res, err := c.Storage.FindCustomerByID(r.Context(), r.PathValue("customer_id"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
func (c Customer) ListCustomersByFirstName(w http.ResponseWriter, r *http.Request) {
fname := r.URL.Query().Get("first_name")
lname := r.URL.Query().Get("last_name")
pk := r.URL.Query().Get("pk")
sk := r.URL.Query().Get("sk")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
asc, _ := strconv.ParseBool(r.URL.Query().Get("asc"))
res, err := c.Storage.ListCustomersByFirstName(r.Context(), fname, lname, pk, sk, limit, asc)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
package api
import (
"encoding/json"
"net/http"
"strconv"
"time"
"dynamodb/ecommerce/src/model/api"
"dynamodb/ecommerce/src/model/storage/database"
"dynamodb/ecommerce/src/storage"
"github.com/google/uuid"
)
type Order struct {
Storage storage.DynamoDB
}
func (o Order) Create(w http.ResponseWriter, r *http.Request) {
var req api.CreateOrder
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
order := database.Order{
ID: uuid.NewString(),
Total: req.Total,
CreatedAt: time.Now(),
CustomerID: req.CustomerID,
}
order.Setup()
products := make([]*database.OrderProduct, 0, len(req.Products))
for _, v := range req.Products {
product := database.OrderProduct{
Quantity: v.Quantity,
Total: v.Total,
OrderID: order.ID,
ProductID: v.ID,
}
product.Setup()
products = append(products, &product)
}
order.Products = products
if err := o.Storage.CreateOrder(r.Context(), order); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(order.ID))
}
func (o Order) FindByID(w http.ResponseWriter, r *http.Request) {
res, err := o.Storage.FindOrderByID(r.Context(), r.PathValue("order_id"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
func (o Order) ListProductsByOrderID(w http.ResponseWriter, r *http.Request) {
orderID := r.PathValue("order_id")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
asc, _ := strconv.ParseBool(r.URL.Query().Get("asc"))
page := r.URL.Query().Get("page")
res, err := o.Storage.ListProductsByOrderID(r.Context(), orderID, limit, asc, page)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
func (o Order) ListOrdersByCustomerID(w http.ResponseWriter, r *http.Request) {
cid := r.PathValue("customer_id")
cat := r.URL.Query().Get("created_at")
pk := r.URL.Query().Get("pk")
sk := r.URL.Query().Get("sk")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
asc, _ := strconv.ParseBool(r.URL.Query().Get("asc"))
res, err := o.Storage.ListOrdersByCustomerID(r.Context(), cid, cat, pk, sk, limit, asc)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
func (o Order) ListOrdersByProductID(w http.ResponseWriter, r *http.Request) {
pid := r.PathValue("product_id")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
asc, _ := strconv.ParseBool(r.URL.Query().Get("asc"))
pk := r.URL.Query().Get("pk")
sk := r.URL.Query().Get("sk")
res, err := o.Storage.ListOrdersByProductID(r.Context(), pid, limit, asc, pk, sk)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
package api
import (
"encoding/json"
"net/http"
"dynamodb/ecommerce/src/model/api"
"dynamodb/ecommerce/src/model/storage/database"
"dynamodb/ecommerce/src/storage"
"github.com/google/uuid"
)
type Product struct {
Storage storage.DynamoDB
}
func (p Product) Create(w http.ResponseWriter, r *http.Request) {
var req api.CreateProduct
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
model := database.Product{
ID: uuid.NewString(),
Name: req.Name,
Price: req.Price,
Tax: req.Tax,
}
model.Setup()
if err := p.Storage.CreateProduct(r.Context(), model); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(model.ID))
}
func (p Product) FindByID(w http.ResponseWriter, r *http.Request) {
res, err := p.Storage.FindProductByID(r.Context(), r.PathValue("product_id"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
bdy, _ := json.Marshal(res)
w.Write(bdy)
}
package api
type CreateCustomer struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
package api
type CreateOrderProduct struct {
ID string `json:"id"`
Quantity int `json:"quantity"`
Total float64 `json:"total"`
}
type CreateOrder struct {
Total float64 `json:"total"`
CustomerID string `json:"customer_id"`
Products []CreateOrderProduct `json:"products"`
}
package api
type CreateProduct struct {
Name string `json:"name"`
Price float64 `json:"price"`
Tax float64 `json:"tax"`
}
package database
type Customer struct {
Item
ID string `dynamodbav:"customer_id"`
FirstName string `dynamodbav:"customer_first_name"`
LastName string `dynamodbav:"customer_last_name"`
}
func (c *Customer) Setup() {
c.Item = Item{
Type: "customer",
PrivateKey: PrivateKey{
Part: "customer_id#" + c.ID,
Sort: "customer_id#" + c.ID,
},
}
}
func (Customer) AttrFirstName() string {
return "customer_first_name"
}
func (Customer) AttrLastName() string {
return "customer_last_name"
}
package database
type Item struct {
PrivateKey
Type string `dynamodbav:"_item_type"`
}
type PrivateKey struct {
Part string `dynamodbav:"_item_part_key"`
Sort string `dynamodbav:"_item_sort_key"`
}
func (PrivateKey) AttrPartKey() string {
return "_item_part_key"
}
func (PrivateKey) AttrSortKey() string {
return "_item_sort_key"
}
package database
import "time"
type Order struct {
Item
ID string `dynamodbav:"order_id"`
Total float64 `dynamodbav:"order_total"`
CreatedAt time.Time `dynamodbav:"order_created_at"`
CustomerID string `dynamodbav:"order_customer_id"`
Products []*OrderProduct `dynamodbav:"-"`
}
func (o *Order) Setup() {
o.Item = Item{
Type: "order",
PrivateKey: PrivateKey{
Part: "order_id#" + o.ID,
Sort: "order_id#" + o.ID,
},
}
}
func (Order) AttrCustomerID() string {
return "customer_id"
}
func (Order) AttrCreatedAt() string {
return "order_created_at"
}
type OrderProduct struct {
Item
Quantity int `dynamodbav:"order_product_quantity"`
Total float64 `dynamodbav:"order_product_total"`
OrderID string `dynamodbav:"order_product_order_id"`
ProductID string `dynamodbav:"order_product_product_id"`
}
func (o *OrderProduct) Setup() {
o.Item = Item{
Type: "order_product",
PrivateKey: PrivateKey{
Part: "order_id#" + o.OrderID,
Sort: "product_id#" + o.ProductID,
},
}
}
func (OrderProduct) AttrProductID() string {
return "order_product_product_id"
}
package database
type Product struct {
Item
ID string `dynamodbav:"product_id"`
Name string `dynamodbav:"product_name"`
Price float64 `dynamodbav:"product_price"`
Tax float64 `dynamodbav:"product_tax"`
}
func (p *Product) Setup() {
p.Item = Item{
Type: "product",
PrivateKey: PrivateKey{
Part: "product_id#" + p.ID,
Sort: "product_id#" + p.ID,
},
}
}
package storage
import (
"context"
"dynamodb/ecommerce/src/app"
"dynamodb/ecommerce/src/model/storage/database"
"fmt"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors"
)
type DynamoDB struct {
Client *dynamodb.Client
Table string
CustomersByNameGSI string
OrdersByCustomerGSI string
OrdersByProductGSI string
}
// CREATE ----------------------------------------------------------------------
func (d DynamoDB) CreateCustomer(ctx context.Context, model database.Customer) error {
item, err := attributevalue.MarshalMap(model)
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
_, err = d.Client.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String(d.Table),
Item: item,
})
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("put item: %s", err.Error()))
}
return nil
}
func (d DynamoDB) CreateProduct(ctx context.Context, model database.Product) error {
item, err := attributevalue.MarshalMap(model)
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
_, err = d.Client.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String(d.Table),
Item: item,
})
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("put item: %s", err.Error()))
}
return nil
}
func (d DynamoDB) CreateOrder(ctx context.Context, model database.Order) error {
items := make([]types.TransactWriteItem, 0, len(model.Products)+1)
for _, product := range model.Products {
item, err := attributevalue.MarshalMap(product)
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
items = append(items, types.TransactWriteItem{
Put: &types.Put{
TableName: aws.String(d.Table),
Item: item,
},
})
}
item, err := attributevalue.MarshalMap(model)
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
items = append(items, types.TransactWriteItem{
Put: &types.Put{
TableName: aws.String(d.Table),
Item: item,
},
})
_, err = d.Client.TransactWriteItems(ctx, &dynamodb.TransactWriteItemsInput{
TransactItems: items,
})
if err != nil {
return errors.Wrap(app.ErrInternal, fmt.Sprintf("put item: %s", err.Error()))
}
return nil
}
// FIND ------------------------------------------------------------------------
func (d DynamoDB) FindCustomerByID(ctx context.Context, id string) (database.Customer, error) {
model := database.Customer{
ID: id,
}
model.Setup()
key, err := attributevalue.MarshalMap(model.PrivateKey)
if err != nil {
return database.Customer{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
res, err := d.Client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String(d.Table),
Key: key,
})
if err != nil {
return database.Customer{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("get item: %s", err.Error()))
}
if res.Item == nil {
return database.Customer{}, app.ErrResourceNotFound
}
if err := attributevalue.UnmarshalMap(res.Item, &model); err != nil {
return database.Customer{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal map: %s", err.Error()))
}
return model, nil
}
func (d DynamoDB) FindProductByID(ctx context.Context, id string) (database.Product, error) {
model := database.Product{
ID: id,
}
model.Setup()
key, err := attributevalue.MarshalMap(model.PrivateKey)
if err != nil {
return database.Product{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
res, err := d.Client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String(d.Table),
Key: key,
})
if err != nil {
return database.Product{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("get item: %s", err.Error()))
}
if res.Item == nil {
return database.Product{}, app.ErrResourceNotFound
}
if err := attributevalue.UnmarshalMap(res.Item, &model); err != nil {
return database.Product{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal map: %s", err.Error()))
}
return model, nil
}
func (d DynamoDB) FindOrderByID(ctx context.Context, id string) (database.Order, error) {
model := database.Order{
ID: id,
}
model.Setup()
key, err := attributevalue.MarshalMap(model.PrivateKey)
if err != nil {
return database.Order{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("marshal map: %s", err.Error()))
}
res, err := d.Client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String(d.Table),
Key: key,
})
if err != nil {
return database.Order{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("get item: %s", err.Error()))
}
if res.Item == nil {
return database.Order{}, app.ErrResourceNotFound
}
if err := attributevalue.UnmarshalMap(res.Item, &model); err != nil {
return database.Order{}, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal map: %s", err.Error()))
}
return model, nil
}
// LIST ------------------------------------------------------------------------
// The page should be enc and dec while using here otherwise passing a similar plain string would still yield a result :(
func (d DynamoDB) ListProductsByOrderID(ctx context.Context, id string, limit int, asc bool, page string) ([]*database.Product, error) {
model := database.OrderProduct{
OrderID: id,
}
model.Setup()
cond1 := expression.Name(model.AttrPartKey()).Equal(expression.Value(model.PrivateKey.Part))
cond2 := expression.Name(model.AttrSortKey()).BeginsWith(model.PrivateKey.Sort)
expr, err := expression.NewBuilder().WithCondition(cond1.And(cond2)).Build()
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("build expression: %s", err.Error()))
}
input := dynamodb.QueryInput{
TableName: aws.String(d.Table),
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression: expr.Condition(),
Limit: aws.Int32(int32(limit) + 1), // Workaround to say there is more pages to paginate
ScanIndexForward: aws.Bool(asc), // true == ASC
}
if page != "" {
input.ExclusiveStartKey = map[string]types.AttributeValue{
model.AttrPartKey(): &types.AttributeValueMemberS{Value: model.PrivateKey.Part},
model.AttrSortKey(): &types.AttributeValueMemberS{Value: page},
}
}
res, err := d.Client.Query(ctx, &input)
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("query: %s", err.Error()))
}
var models []*database.Product
if err := attributevalue.UnmarshalListOfMaps(res.Items, &models); err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal list of maps: %s", err.Error()))
}
// Workaround to say there is more pages to paginate
if total := len(models); total == limit+1 {
models = models[:total-1]
if total := len(models); total != 0 {
fmt.Printf("NEXT PAGE: %+v\n", models[len(models)-1].PrivateKey.Sort)
}
}
return models, nil
}
// The page should be enc and dec while using here otherwise passing a similar plain string would still yield a result :(
func (d DynamoDB) ListCustomersByFirstName(ctx context.Context, fname, lname string, pk, sk string, limit int, asc bool) ([]*database.Customer, error) {
var model database.Customer
cond := expression.Name(model.AttrFirstName()).Equal(expression.Value(fname))
expr, err := expression.NewBuilder().WithCondition(cond).Build()
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("build expression: %s", err.Error()))
}
input := dynamodb.QueryInput{
TableName: aws.String(d.Table),
IndexName: aws.String(d.CustomersByNameGSI),
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression: expr.Condition(),
Limit: aws.Int32(int32(limit) + 1), // Workaround to say there is more pages to paginate
ScanIndexForward: aws.Bool(asc), // true == ASC
}
if fname != "" && lname != "" && pk != "" && sk != "" {
input.ExclusiveStartKey = map[string]types.AttributeValue{
model.AttrPartKey(): &types.AttributeValueMemberS{Value: pk},
model.AttrSortKey(): &types.AttributeValueMemberS{Value: sk},
model.AttrFirstName(): &types.AttributeValueMemberS{Value: fname},
model.AttrLastName(): &types.AttributeValueMemberS{Value: lname},
}
}
res, err := d.Client.Query(ctx, &input)
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("query: %s", err.Error()))
}
var models []*database.Customer
if err := attributevalue.UnmarshalListOfMaps(res.Items, &models); err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal list of maps: %s", err.Error()))
}
// Workaround to say there is more pages to paginate
if total := len(models); total == limit+1 {
models = models[:total-1]
if total := len(models); total != 0 {
fmt.Printf("PK: %+v\n", models[len(models)-1].PrivateKey.Part)
fmt.Printf("SK: %+v\n", models[len(models)-1].PrivateKey.Sort)
fmt.Printf("LN: %+v\n", models[len(models)-1].LastName)
}
}
return models, nil
}
// The page should be enc and dec while using here otherwise passing a similar plain string would still yield a result :(
func (d DynamoDB) ListOrdersByCustomerID(ctx context.Context, cid, cat, pk, sk string, limit int, asc bool) ([]*database.Order, error) {
var model database.Order
cond := expression.Name(model.AttrCustomerID()).Equal(expression.Value(cid))
expr, err := expression.NewBuilder().WithCondition(cond).Build()
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("build expression: %s", err.Error()))
}
input := dynamodb.QueryInput{
TableName: aws.String(d.Table),
IndexName: aws.String(d.OrdersByCustomerGSI),
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression: expr.Condition(),
Limit: aws.Int32(int32(limit) + 1), // Workaround to say there is more pages to paginate
ScanIndexForward: aws.Bool(asc), // true == ASC
}
if cid != "" && cat != "" && pk != "" && sk != "" {
input.ExclusiveStartKey = map[string]types.AttributeValue{
model.AttrPartKey(): &types.AttributeValueMemberS{Value: pk},
model.AttrSortKey(): &types.AttributeValueMemberS{Value: sk},
model.AttrCustomerID(): &types.AttributeValueMemberS{Value: cid},
model.AttrCreatedAt(): &types.AttributeValueMemberS{Value: strings.ReplaceAll(cat, " ", "+")},
}
}
res, err := d.Client.Query(ctx, &input)
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("query: %s", err.Error()))
}
var models []*database.Order
if err := attributevalue.UnmarshalListOfMaps(res.Items, &models); err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal list of maps: %s", err.Error()))
}
// Workaround to say there is more pages to paginate
if total := len(models); total == limit+1 {
models = models[:total-1]
if total := len(models); total != 0 {
fmt.Printf("PK: %+v\n", models[len(models)-1].PrivateKey.Part)
fmt.Printf("SK: %+v\n", models[len(models)-1].PrivateKey.Sort)
fmt.Printf("LN: %+v\n", models[len(models)-1].CreatedAt) // Copy this from Postman/DB because it is formatter in terminal
}
}
return models, nil
}
// The page should be enc and dec while using here otherwise passing a similar plain string would still yield a result :(
func (d DynamoDB) ListOrdersByProductID(ctx context.Context, pid string, limit int, asc bool, pk, sk string) ([]*database.OrderProduct, error) {
var model database.OrderProduct
cond := expression.Name(model.AttrProductID()).Equal(expression.Value(pid))
expr, err := expression.NewBuilder().WithCondition(cond).Build()
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("build expression: %s", err.Error()))
}
input := dynamodb.QueryInput{
TableName: aws.String(d.Table),
IndexName: aws.String(d.OrdersByProductGSI),
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression: expr.Condition(),
Limit: aws.Int32(int32(limit) + 1), // Workaround to say there is more pages to paginate
ScanIndexForward: aws.Bool(asc), // true == ASC
}
if pk != "" && sk != "" {
input.ExclusiveStartKey = map[string]types.AttributeValue{
model.AttrPartKey(): &types.AttributeValueMemberS{Value: pk},
model.AttrSortKey(): &types.AttributeValueMemberS{Value: sk},
model.AttrProductID(): &types.AttributeValueMemberS{Value: pid},
}
}
res, err := d.Client.Query(ctx, &input)
if err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("query: %s", err.Error()))
}
var models []*database.OrderProduct
if err := attributevalue.UnmarshalListOfMaps(res.Items, &models); err != nil {
return nil, errors.Wrap(app.ErrInternal, fmt.Sprintf("unmarshal list of maps: %s", err.Error()))
}
// Workaround to say there is more pages to paginate
if total := len(models); total == limit+1 {
models = models[:total-1]
if total := len(models); total != 0 {
fmt.Printf("NEXT PAGE (PK): %+v\n", models[len(models)-1].PrivateKey.Part)
fmt.Printf("NEXT PAGE (SK): %+v\n", models[len(models)-1].PrivateKey.Sort)
}
}
return models, nil
}