Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A comprehensive serverless API platform built with Go, demonstrating modern clou
- **G**o - Modern, efficient programming language with excellent performance and concurrency
- **L**ambda - AWS serverless compute platform for running code without managing servers
- **A**piGateway - AWS managed API gateway service for creating, deploying, and managing REST APIs
- **D**ynamoDB - AWS NoSQL database service providing fast and predictable performance with seamless scalability
- **D**ynamoDB - AWS NoSQL database(Single Table Design) service providing fast and predictable performance with seamless scalability

This project showcases how these four technologies work together to create a production-ready, scalable, and cost-effective serverless API platform that can handle millions of requests while maintaining low latency and high availability.

Expand Down Expand Up @@ -270,7 +270,7 @@ Structured logging with slog, including request duration tracking.

Deployed resources (via CDK):

- **DynamoDB Table**: `users` table with username as partition key. Single Table Design - progress ⏳
- **DynamoDB Table**: `glad-entities` table
- **Lambda Function**: Go 1.x runtime with provided.al2023
- **API Gateway**: REST API with CORS enabled
- **IAM Roles**: Least-privilege access for Lambda
Expand Down
4 changes: 3 additions & 1 deletion cmd/app/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ func testConfig() *config.Config {
// SetupIntegrationTest creates a test environment
func SetupIntegrationTest() *IntegrationTestSuite {
userRepo := database.NewMockRepository()
userSkillsRepo := database.NewMockRepository()
tokenService := auth.NewTokenService(testConfig())
userService := service.NewUserService(userRepo, tokenService)
apiHandler := handler.New(userService)
userSkillsService := service.NewSkillService(userSkillsRepo)
apiHandler := handler.New(userService, userSkillsService)
authMiddleware := middleware.NewAuthMiddleware(tokenService)

// Create HTTP server with the same routing as local-server.go
Expand Down
180 changes: 7 additions & 173 deletions cmd/app/internal/database/dynamodb.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,29 @@
package database

import (
"time"

apperrors "github.com/hackmajoris/glad/cmd/app/internal/errors"
"github.com/hackmajoris/glad/cmd/app/internal/models"
"github.com/hackmajoris/glad/pkg/logger"

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

const (
UsersTableName = "users"
)
// TableName is the single table for all entities
TableName = "glad-entities"

// UserRepository defines the interface for user data operations
type UserRepository interface {
CreateUser(user *models.User) error
GetUser(username string) (*models.User, error)
UpdateUser(user *models.User) error
UserExists(username string) (bool, error)
ListUsers() ([]*models.User, error)
}
// GSI1Name GSI names
GSI1Name = "GSI1"
)

// DynamoDBRepository implements UserRepository using DynamoDB
// DynamoDBRepository implements Repository using DynamoDB single table design
type DynamoDBRepository struct {
client *dynamodb.DynamoDB
}

// NewDynamoDBRepository creates a new DynamoDB repository
func NewDynamoDBRepository() *DynamoDBRepository {
log := logger.WithComponent("database")
log.Info("Initializing DynamoDB repository", "table", UsersTableName)
log.Info("Initializing DynamoDB repository", "table", TableName)

sess := session.Must(session.NewSession())
repo := &DynamoDBRepository{
Expand All @@ -44,158 +33,3 @@ func NewDynamoDBRepository() *DynamoDBRepository {
log.Info("DynamoDB repository initialized successfully")
return repo
}

// CreateUser inserts a new user into DynamoDB
func (r *DynamoDBRepository) CreateUser(user *models.User) error {
log := logger.WithComponent("database").With("operation", "CreateUser", "username", user.Username)
start := time.Now()

log.Debug("Starting user creation")

item, err := dynamodbattribute.MarshalMap(user)
if err != nil {
log.Error("Failed to marshal user data", "error", err.Error(), "duration", time.Since(start))
return err
}

input := &dynamodb.PutItemInput{
TableName: aws.String(UsersTableName),
Item: item,
ConditionExpression: aws.String("attribute_not_exists(username)"),
}

_, err = r.client.PutItem(input)
if err != nil {
log.Error("Failed to create user in DynamoDB", "error", err.Error(), "duration", time.Since(start))
return err
}

log.Info("User created successfully", "duration", time.Since(start))
return nil
}

// GetUser retrieves a user by username from DynamoDB
func (r *DynamoDBRepository) GetUser(username string) (*models.User, error) {
log := logger.WithComponent("database").With("operation", "GetUser", "username", username)
start := time.Now()

log.Debug("Starting user retrieval")

input := &dynamodb.GetItemInput{
TableName: aws.String(UsersTableName),
Key: map[string]*dynamodb.AttributeValue{
"username": {
S: aws.String(username),
},
},
}

result, err := r.client.GetItem(input)
if err != nil {
log.Error("Failed to get user from DynamoDB", "error", err.Error(), "duration", time.Since(start))
return nil, err
}

if result.Item == nil {
log.Debug("User not found", "duration", time.Since(start))
return nil, apperrors.ErrUserNotFound
}

var user models.User
err = dynamodbattribute.UnmarshalMap(result.Item, &user)
if err != nil {
log.Error("Failed to unmarshal user data", "error", err.Error(), "duration", time.Since(start))
return nil, err
}

log.Debug("User retrieved successfully", "duration", time.Since(start))
return &user, nil
}

// UserExists checks if a user exists in DynamoDB
func (r *DynamoDBRepository) UserExists(username string) (bool, error) {
log := logger.WithComponent("database").With("operation", "UserExists", "username", username)
start := time.Now()

log.Debug("Checking if user exists")

input := &dynamodb.GetItemInput{
TableName: aws.String(UsersTableName),
Key: map[string]*dynamodb.AttributeValue{
"username": {
S: aws.String(username),
},
},
ProjectionExpression: aws.String("username"),
}

result, err := r.client.GetItem(input)
if err != nil {
log.Error("Failed to check user existence", "error", err.Error(), "duration", time.Since(start))
return false, err
}

exists := result.Item != nil
log.Debug("User existence check completed", "exists", exists, "duration", time.Since(start))
return exists, nil
}

// UpdateUser updates an existing user in DynamoDB
func (r *DynamoDBRepository) UpdateUser(user *models.User) error {
log := logger.WithComponent("database").With("operation", "UpdateUser", "username", user.Username)
start := time.Now()

log.Debug("Starting user update")

item, err := dynamodbattribute.MarshalMap(user)
if err != nil {
log.Error("Failed to marshal user data for update", "error", err.Error(), "duration", time.Since(start))
return err
}

input := &dynamodb.PutItemInput{
TableName: aws.String(UsersTableName),
Item: item,
ConditionExpression: aws.String("attribute_exists(username)"),
}

_, err = r.client.PutItem(input)
if err != nil {
log.Error("Failed to update user in DynamoDB", "error", err.Error(), "duration", time.Since(start))
return err
}

log.Info("User updated successfully", "duration", time.Since(start))
return nil
}

// ListUsers retrieves all users from DynamoDB
func (r *DynamoDBRepository) ListUsers() ([]*models.User, error) {
log := logger.WithComponent("database").With("operation", "ListUsers")
start := time.Now()

log.Debug("Starting users list retrieval")

input := &dynamodb.ScanInput{
TableName: aws.String(UsersTableName),
}

result, err := r.client.Scan(input)
if err != nil {
log.Error("Failed to scan users table", "error", err.Error(), "duration", time.Since(start))
return nil, err
}

var users []*models.User
for i, item := range result.Items {
var user models.User
if err := dynamodbattribute.UnmarshalMap(item, &user); err != nil {
log.Error("Failed to unmarshal user data", "error", err.Error(), "item_index", i, "duration", time.Since(start))
return nil, err
}
users = append(users, &user)
}

log.Info("Users retrieved successfully", "count", len(users), "scanned_count", *result.ScannedCount, "duration", time.Since(start))
return users, nil
}
25 changes: 25 additions & 0 deletions cmd/app/internal/database/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package database

import (
"github.com/hackmajoris/glad/pkg/config"
"github.com/hackmajoris/glad/pkg/logger"
)

// Repository combines all repository interfaces for unified access
type Repository interface {
UserRepository
SkillRepository
}

// NewRepository creates the appropriate repository implementation based on configuration
func NewRepository(cfg *config.Config) Repository {
log := logger.WithComponent("database")

if cfg.LocalServer.Environment == "development" || cfg.LocalServer.Environment == "test" {
log.Info("Creating Mock repository for development/testing")
return NewMockRepository()
}

log.Info("Creating DynamoDB repository for production")
return NewDynamoDBRepository()
}
Loading
Loading