Bu örnekte e-ticaret sitesini simüle edeceğiz ve veritabanı olarak DynamoDB kullanacağız. Basit bir tabloyla birlikte, öğeleri bulmak veya listelemek için GSI'leri kullanacağız.


Dikkat edin, buradaki dosyaların çoğu oldukça kabataslak, dolayısıyla onları geliştirmek size kalmış.


Modeller


GSI'ların temel tablodan alakasız öğeleri çekmesini önlemek için bilerek tüm özniteliklerin önüne modelden sonrasını ekledim.


- 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

Erişim desenleri


Ana tablo


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

Küresel İkincil Endeks


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

Yapı


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

Dosyalar


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
}