Skip to content
Permalink
Browse files
Introducing basic CQRS
  • Loading branch information
roblaszczak committed Sep 27, 2020
1 parent a638f03 commit 8d9274811559399461aa9f6bf3829316b8ddfb63
Show file tree
Hide file tree
Showing 62 changed files with 2,668 additions and 870 deletions.
@@ -44,6 +44,10 @@ lint:
@./scripts/lint.sh trainings
@./scripts/lint.sh users

.PHONY: fmt
fmt:
goimports -l -w internal/

.PHONY: mycli
mycli:
mycli -u ${MYSQL_USER} -p ${MYSQL_PASSWORD} ${MYSQL_DATABASE}
@@ -22,7 +22,8 @@ No application is perfect from the beginning. With over a dozen coming articles,
7. [**Repository pattern: painless way to simplify your Go service logic**](https://threedots.tech/post/repository-pattern-in-go/?utm_source=github.com) _[[v2.2]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.2)_
8. [**4 practical principles of high-quality database integration tests in Go**](https://threedots.tech/post/database-integration-testing/?utm_source=github.com) _[[v2.3]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.3)_
9. [**Introducing Clean Architecture by refactoring a Go project**](https://threedots.tech/post/introducing-clean-architecture/?utm_source=github.com) _[[v2.4]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.4)_
10. *More articles are on the way!*
10. [**Introducing basic CQRS by refactoring**](https://threedots.tech/post/basic-cqrs-in-go/?utm_source=github.com) _[[v2.5]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.5)_
11. *More articles are on the way!*

### Directories

@@ -99,6 +99,33 @@ paths:
schema:
$ref: '#/components/schemas/Error'

/trainings/{trainingUUID}/request-reschedule:
put:
operationId: requestRescheduleTraining
requestBody:
description: todo
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PostTraining'
parameters:
- in: path
name: trainingUUID
schema:
type: string
format: uuid
required: true
description: todo
responses:
'204':
description: todo
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/trainings/{trainingUUID}/approve-reschedule:
put:

Some generated files are not rendered by default. Learn more.

Some generated files are not rendered by default. Learn more.

@@ -0,0 +1,15 @@
package logs

import (
"github.com/sirupsen/logrus"
)

func LogCommandExecution(commandName string, cmd interface{}, err error) {
log := logrus.WithField("cmd", cmd)

if err == nil {
log.Info(commandName + " command succeeded")
} else {
log.WithError(err).Error(commandName + " command failed")
}
}
@@ -2,9 +2,11 @@ package adapters

import (
"context"
"sort"
"time"

"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app"
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query"
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour"

"cloud.google.com/go/firestore"
"google.golang.org/api/iterator"
@@ -24,16 +26,15 @@ type HourModel struct {

type DatesFirestoreRepository struct {
firestoreClient *firestore.Client
factoryConfig hour.FactoryConfig
}

func NewDatesFirestoreRepository(firestoreClient *firestore.Client) DatesFirestoreRepository {
func NewDatesFirestoreRepository(firestoreClient *firestore.Client, factoryConfig hour.FactoryConfig) DatesFirestoreRepository {
if firestoreClient == nil {
panic("missing firestoreClient")
}

return DatesFirestoreRepository{
firestoreClient: firestoreClient,
}
return DatesFirestoreRepository{firestoreClient, factoryConfig}
}

func (d DatesFirestoreRepository) trainerHoursCollection() *firestore.CollectionRef {
@@ -44,14 +45,14 @@ func (d DatesFirestoreRepository) DocumentRef(dateTimeToUpdate time.Time) *fires
return d.trainerHoursCollection().Doc(dateTimeToUpdate.Format("2006-01-02"))
}

func (d DatesFirestoreRepository) GetDates(ctx context.Context, from time.Time, to time.Time) ([]app.Date, error) {
func (d DatesFirestoreRepository) AvailableHours(ctx context.Context, from time.Time, to time.Time) ([]query.Date, error) {
iter := d.
trainerHoursCollection().
Where("Date", ">=", from).
Where("Date", "<=", to).
Documents(ctx)

var dates []app.Date
var dates []query.Date

for {
doc, err := iter.Next()
@@ -69,31 +70,73 @@ func (d DatesFirestoreRepository) GetDates(ctx context.Context, from time.Time,
dates = append(dates, dateModelToApp(date))
}

dates = addMissingDates(dates, from, to)
for i, date := range dates {
date = d.setDefaultAvailability(date)
sort.Slice(date.Hours, func(i, j int) bool { return date.Hours[i].Hour.Before(date.Hours[j].Hour) })
dates[i] = date
}
sort.Slice(dates, func(i, j int) bool { return dates[i].Date.Before(dates[j].Date) })

return dates, nil
}

func dateModelToApp(dm DateModel) app.Date {
var hours []app.Hour
// setDefaultAvailability adds missing hours to Date model if they were not set
func (d DatesFirestoreRepository) setDefaultAvailability(date query.Date) query.Date {
HoursLoop:
for h := d.factoryConfig.MinUtcHour; h <= d.factoryConfig.MaxUtcHour; h++ {
hour := time.Date(date.Date.Year(), date.Date.Month(), date.Date.Day(), h, 0, 0, 0, time.UTC)

for i := range date.Hours {
if date.Hours[i].Hour.Equal(hour) {
continue HoursLoop
}
}
newHour := query.Hour{
Available: false,
Hour: hour,
}

date.Hours = append(date.Hours, newHour)
}

return date
}

func addMissingDates(dates []query.Date, from time.Time, to time.Time) []query.Date {
for day := from.UTC(); day.Before(to) || day.Equal(to); day = day.AddDate(0, 0, 1) {
found := false
for _, date := range dates {
if date.Date.Equal(day) {
found = true
break
}
}

if !found {
date := query.Date{
Date: day,
}
dates = append(dates, date)
}
}

return dates
}

func dateModelToApp(dm DateModel) query.Date {
var hours []query.Hour
for _, h := range dm.Hours {
hours = append(hours, app.Hour{
hours = append(hours, query.Hour{
Available: h.Available,
HasTrainingScheduled: h.HasTrainingScheduled,
Hour: h.Hour,
})
}

return app.Date{
return query.Date{
Date: dm.Date,
HasFreeHours: dm.HasFreeHours,
Hours: hours,
}
}

func (d DatesFirestoreRepository) CanLoadFixtures(ctx context.Context, daysToSet int) (bool, error) {
documents, err := d.trainerHoursCollection().Limit(daysToSet).Documents(ctx).GetAll()
if err != nil {
return false, err
}

return len(documents) < daysToSet, nil
}
@@ -79,10 +79,28 @@ func (m MySQLHourRepository) getOrCreateHour(
return domainHour, nil
}

const mySQLDeadlockErrorCode = 1213

func (m MySQLHourRepository) UpdateHour(
ctx context.Context,
hourTime time.Time,
updateFn func(h *hour.Hour) (*hour.Hour, error),
) error {
for {
err := m.updateHour(ctx, hourTime, updateFn)

if val, ok := err.(*mysql.MySQLError); ok && val.Number == mySQLDeadlockErrorCode {
continue
}

return err
}
}

func (m MySQLHourRepository) updateHour(
ctx context.Context,
hourTime time.Time,
updateFn func(h *hour.Hour) (*hour.Hour, error),
) (err error) {
tx, err := m.db.Beginx()
if err != nil {
@@ -290,10 +290,10 @@ var testHourFactory = hour.MustNewFactory(hour.FactoryConfig{
})

func newFirebaseRepository(t *testing.T, ctx context.Context) *adapters.FirestoreHourRepository {
firebaseClient, err := firestore.NewClient(ctx, os.Getenv("GCP_PROJECT"))
firestoreClient, err := firestore.NewClient(ctx, os.Getenv("GCP_PROJECT"))
require.NoError(t, err)

return adapters.NewFirestoreHourRepository(firebaseClient, testHourFactory)
return adapters.NewFirestoreHourRepository(firestoreClient, testHourFactory)
}

func newMySQLRepository(t *testing.T) *adapters.MySQLHourRepository {
@@ -0,0 +1,24 @@
package app

import (
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/command"
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query"
)

type Application struct {
Commands Commands
Queries Queries
}

type Commands struct {
CancelTraining command.CancelTrainingHandler
ScheduleTraining command.ScheduleTrainingHandler

MakeHoursAvailable command.MakeHoursAvailableHandler
MakeHoursUnavailable command.MakeHoursUnavailableHandler
}

type Queries struct {
HourAvailability query.HourAvailabilityHandler
TrainerAvailableHours query.AvailableHoursHandler
}
@@ -0,0 +1,34 @@
package command

import (
"context"
"time"

"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors"
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour"
)

type CancelTrainingHandler struct {
hourRepo hour.Repository
}

func NewCancelTrainingHandler(hourRepo hour.Repository) CancelTrainingHandler {
if hourRepo == nil {
panic("nil hourRepo")
}

return CancelTrainingHandler{hourRepo: hourRepo}
}

func (h CancelTrainingHandler) Handle(ctx context.Context, hourToCancel time.Time) error {
if err := h.hourRepo.UpdateHour(ctx, hourToCancel, func(h *hour.Hour) (*hour.Hour, error) {
if err := h.CancelTraining(); err != nil {
return nil, err
}
return h, nil
}); err != nil {
return errors.NewSlugError(err.Error(), "unable-to-update-availability")
}

return nil
}
@@ -0,0 +1,36 @@
package command

import (
"context"
"time"

"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors"
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour"
)

type MakeHoursAvailableHandler struct {
hourRepo hour.Repository
}

func NewMakeHoursAvailableHandler(hourRepo hour.Repository) MakeHoursAvailableHandler {
if hourRepo == nil {
panic("hourRepo is nil")
}

return MakeHoursAvailableHandler{hourRepo: hourRepo}
}

func (c MakeHoursAvailableHandler) Handle(ctx context.Context, hours []time.Time) error {
for _, hourToUpdate := range hours {
if err := c.hourRepo.UpdateHour(ctx, hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) {
if err := h.MakeAvailable(); err != nil {
return nil, err
}
return h, nil
}); err != nil {
return errors.NewSlugError(err.Error(), "unable-to-update-availability")
}
}

return nil
}

0 comments on commit 8d92748

Please sign in to comment.