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.


Models


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

Access patterns


Base table


# 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#")

Global Secondary Index


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

Structure


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

Files


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

Makefile


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

main.go


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

app/error.go


package app

import (
"errors"
)

var (
ErrInternal = errors.New("internal")
ErrResourceNotFound = errors.New("resource not found")
)

api/customer.go


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

api/order.go


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

api/order.go


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

model/customer.go


package api

type CreateCustomer struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}

model/order.go


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

model/product.go


package api

type CreateProduct struct {
Name string `json:"name"`
Price float64 `json:"price"`
Tax float64 `json:"tax"`
}

database/customer.go


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

database/item.go


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

database/order.go


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

database/product.go


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

dynamodb.go


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
}