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
Empty file.
415 changes: 325 additions & 90 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ tasks:
- go test -coverprofile=coverage.out ./...
- go tool cover -html=coverage.out -o coverage.html
- echo 'Coverage report generated - coverage.html'
- go tool cover -html=coverage.out

test:suite:
desc: 'Run comprehensive test suite with detailed reporting'
Expand Down
58 changes: 58 additions & 0 deletions cmd/app/internal/database/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package database

import (
"sync"

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

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
)

// DynamoDBRepository implements all repository interfaces using DynamoDB single table design
// It provides implementations for:
// - UserRepository (user management)
// - MasterSkillRepository (master skills)
// - SkillRepository (user skills)
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", TableName)

sess := session.Must(session.NewSession())
repo := &DynamoDBRepository{
client: dynamodb.New(sess),
}

log.Info("DynamoDB repository initialized successfully")
return repo
}

// MockRepository implements UserRepository, SkillRepository, and MasterSkillRepository for testing
// This matches the DynamoDBRepository structure with unified implementation
type MockRepository struct {
users map[string]*models.User // key: username
skills map[string]*models.UserSkill // key: "username#skillname"
masterSkills map[string]*models.Skill // key: skill_id
mutex sync.RWMutex
}

// NewMockRepository creates a new unified mock repository
func NewMockRepository() *MockRepository {
log := logger.WithComponent("database")
log.Info("Initializing unified Mock repository for local development")

repo := &MockRepository{
users: make(map[string]*models.User),
skills: make(map[string]*models.UserSkill),
masterSkills: make(map[string]*models.Skill),
}

log.Info("Unified Mock repository initialized successfully")
return repo
}
31 changes: 0 additions & 31 deletions cmd/app/internal/database/dynamodb.go

This file was deleted.

3 changes: 2 additions & 1 deletion cmd/app/internal/database/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
"github.com/hackmajoris/glad/pkg/logger"
)

// Repository combines all repository interfaces for unified access
// Repository combines all repository interfaces for unified access.
// Both DynamoDBRepository and MockRepository implement this interface.
type Repository interface {
UserRepository
SkillRepository
Expand Down
177 changes: 1 addition & 176 deletions cmd/app/internal/database/master_skill_repository.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
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/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
import "github.com/hackmajoris/glad/cmd/app/internal/models"

// MasterSkillRepository defines operations for master skills
type MasterSkillRepository interface {
Expand All @@ -20,168 +10,3 @@ type MasterSkillRepository interface {
DeleteMasterSkill(skillID string) error
ListMasterSkills() ([]*models.Skill, error)
}

// CreateMasterSkill inserts a new master skill
func (r *DynamoDBRepository) CreateMasterSkill(skill *models.Skill) error {
log := logger.WithComponent("database").With("operation", "CreateMasterSkill", "skill_id", skill.SkillID)
start := time.Now()

log.Debug("Starting master skill creation")

skill.SetKeys()

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

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

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

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

// GetMasterSkill retrieves a master skill by ID
func (r *DynamoDBRepository) GetMasterSkill(skillID string) (*models.Skill, error) {
log := logger.WithComponent("database").With("operation", "GetMasterSkill", "skill_id", skillID)
start := time.Now()

log.Debug("Starting master skill retrieval")

entityID := BuildMasterSkillEntityID(skillID)

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

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

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

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

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

// UpdateMasterSkill updates an existing master skill
func (r *DynamoDBRepository) UpdateMasterSkill(skill *models.Skill) error {
log := logger.WithComponent("database").With("operation", "UpdateMasterSkill", "skill_id", skill.SkillID)
start := time.Now()

log.Debug("Starting master skill update")

skill.SetKeys()
skill.UpdatedAt = time.Now()

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

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

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

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

// DeleteMasterSkill removes a master skill
func (r *DynamoDBRepository) DeleteMasterSkill(skillID string) error {
log := logger.WithComponent("database").With("operation", "DeleteMasterSkill", "skill_id", skillID)
start := time.Now()

log.Debug("Starting master skill deletion")

entityID := BuildMasterSkillEntityID(skillID)

input := &dynamodb.DeleteItemInput{
TableName: aws.String(TableName),
Key: map[string]*dynamodb.AttributeValue{
"entity_id": {S: aws.String(entityID)},
},
ConditionExpression: aws.String("attribute_exists(entity_id)"),
}

_, err := r.client.DeleteItem(input)
if err != nil {
log.Error("Failed to delete master skill from DynamoDB", "error", err.Error(), "duration", time.Since(start))
return apperrors.ErrSkillNotFound
}

log.Info("Master skill deleted successfully", "duration", time.Since(start))
return nil
}

// ListMasterSkills retrieves all master skills
func (r *DynamoDBRepository) ListMasterSkills() ([]*models.Skill, error) {
log := logger.WithComponent("database").With("operation", "ListMasterSkills")
start := time.Now()

log.Debug("Starting master skills list retrieval")

// Use GSI for better performance instead of Scan
input := &dynamodb.QueryInput{
TableName: aws.String(TableName),
IndexName: aws.String(GSIByEntityType),
KeyConditionExpression: aws.String("EntityType = :entityType"),
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":entityType": {S: aws.String("Skill")},
},
}

result, err := r.client.Query(input)
if err != nil {
log.Error("Failed to query master skills", "error", err.Error(), "duration", time.Since(start))
return nil, err
}

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

log.Info("Master skills retrieved successfully", "count", len(skills), "duration", time.Since(start))
return skills, nil
}
Loading
Loading