Skip to content

Commit

Permalink
Change architecture to CQRS event sourcing
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Baecher committed Feb 22, 2018
1 parent a038885 commit e347004
Show file tree
Hide file tree
Showing 16 changed files with 507 additions and 567 deletions.
75 changes: 2 additions & 73 deletions api/environment_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"github.com/DataDog/datadog-go/statsd"
"github.com/labstack/echo"

"github.com/MEDIGO/laika/models"
"github.com/MEDIGO/laika/store"
)

Expand All @@ -17,81 +16,11 @@ func NewEnvironmentResource(store store.Store, stats *statsd.Client) *Environmen
return &EnvironmentResource{store, stats}
}

func (r *EnvironmentResource) Get(c echo.Context) error {
name := c.Param("name")

environment, err := r.store.GetEnvironmentByName(name)
if err != nil {
if err == store.ErrNoRows {
return NotFound(c)
}

return InternalServerError(c, err)
}

return OK(c, environment)
}

func (r *EnvironmentResource) List(c echo.Context) error {
environments, err := r.store.ListEnvironments()
if err != nil {
return InternalServerError(c, err)
}

return OK(c, environments)
}

func (r *EnvironmentResource) Create(c echo.Context) error {
input := struct {
Name string `json:"name"`
}{}

if err := c.Bind(&input); err != nil {
return BadRequest(c, "Payload must be a valid JSON object")
}

if input.Name == "" {
return Invalid(c, "Name is required")
}

environment := &models.Environment{
Name: input.Name,
}

if err := r.store.CreateEnvironment(environment); err != nil {
return InternalServerError(c, err)
}

return Created(c, environment)
}

func (r *EnvironmentResource) Update(c echo.Context) error {
name := c.Param("name")

environment, err := r.store.GetEnvironmentByName(name)
s, err := r.store.State()
if err != nil {
if err == store.ErrNoRows {
return NotFound(c)
}

return InternalServerError(c, err)
}

input := struct {
Name string `json:"name"`
}{}

if err := c.Bind(&input); err != nil {
return BadRequest(c, "Payload must be a valid JSON object")
}

if input.Name != "" {
environment.Name = input.Name
}

if err := r.store.UpdateEnvironment(environment); err != nil {
return InternalServerError(c, err)
}

return OK(c, environment)
return OK(c, s.Environments)
}
64 changes: 64 additions & 0 deletions api/event_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package api

import (
"encoding/json"

"github.com/DataDog/datadog-go/statsd"
"github.com/MEDIGO/laika/models"
"github.com/MEDIGO/laika/notifier"
"github.com/MEDIGO/laika/store"
"github.com/labstack/echo"
)

type EventResource struct {
store store.Store
stats *statsd.Client
notifier notifier.Notifier
}

func NewEventResource(store store.Store, stats *statsd.Client, notifier notifier.Notifier) *EventResource {
return &EventResource{store, stats, notifier}
}

func (r *EventResource) Create(c echo.Context) error {
eventType := c.Param("type")
event, err := models.EventForType(eventType)
if err != nil {
return Invalid(c, err.Error())
}

if err := c.Bind(&event); err != nil {
return BadRequest(c, "Body must be a valid JSON object")
}

state, err := r.store.State()
if err != nil {
return InternalServerError(c, err)
}

valErr, err := event.Validate(state)
if err != nil {
return InternalServerError(c, err)
} else if valErr != nil {
return BadRequest(c, valErr.Error())
}

event, err = event.PrePersist(state)
if err != nil {
return InternalServerError(c, err)
}

cleanEvent, err := json.Marshal(event)
if err != nil {
return InternalServerError(c, err)
}

id, err := r.store.Persist(eventType, string(cleanEvent))
if err != nil {
return InternalServerError(c, err)
}

return Created(c, struct {
ID int64 `json:"id"`
}{ID: id})
}
114 changes: 27 additions & 87 deletions api/feature_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/MEDIGO/laika/models"
"github.com/MEDIGO/laika/notifier"
"github.com/MEDIGO/laika/store"
log "github.com/Sirupsen/logrus"
"github.com/labstack/echo"
)

Expand All @@ -27,109 +26,50 @@ func (r *FeatureResource) Get(c echo.Context) error {
return BadRequest(c, "Bad feature name")
}

feature, err := r.store.GetFeatureByName(name)
state, err := r.store.State()
if err != nil {
if err == store.ErrNoRows {
return NotFound(c)
}
return InternalServerError(c, err)
}

return OK(c, feature)
}

func (r *FeatureResource) List(c echo.Context) error {
features, err := r.store.ListFeatures()
if err != nil {
if err == store.ErrNoRows {
return NotFound(c)
for _, feature := range state.Features {
if feature.Name == name {
return OK(c, *getFeatureStatus(&feature, state))
}
return InternalServerError(c, err)
}

return OK(c, features)
return NotFound(c)
}

func (r *FeatureResource) Create(c echo.Context) error {
input := struct {
Name string `json:"name"`
}{}

if err := c.Bind(&input); err != nil {
return BadRequest(c, "Payload must be a valid JSON object")
}

if input.Name == "" {
return Invalid(c, "Name is required")
}

found, err := r.store.GetFeatureByName(input.Name)
if err != nil && err != store.ErrNoRows {
return InternalServerError(c, err)
}

if found != nil {
return Conflict(c, "Feature already exists")
}

feature := &models.Feature{
Name: input.Name,
}

if err := r.store.CreateFeature(feature); err != nil {
return InternalServerError(c, err)
}

return OK(c, feature)
}

func (r *FeatureResource) Update(c echo.Context) error {
name := c.Param("name")

feature, err := r.store.GetFeatureByName(name)
func (r *FeatureResource) List(c echo.Context) error {
state, err := r.store.State()
if err != nil {
if err == store.ErrNoRows {
return NotFound(c)
}
return InternalServerError(c, err)
}

input := struct {
Name string `json:"name"`
Status map[string]bool `json:"status"`
}{}

if err := c.Bind(&input); err != nil {
return BadRequest(c, "Payload must be a valid JSON object")
}

if input.Name != "" {
feature.Name = input.Name
status := []featureStatus{}
for _, feature := range state.Features {
status = append(status, *getFeatureStatus(&feature, state))
}
return OK(c, status)
}

// keep the previous status so we can notify changes
prevStats := make(map[string]bool)
for name, enabled := range feature.Status {
prevStats[name] = enabled
func getFeatureStatus(feature *models.Feature, s *models.State) *featureStatus {
fs := featureStatus{
Feature: *feature,
Status: map[string]bool{},
}

if input.Status != nil {
feature.Status = input.Status
for _, env := range s.Environments {
enabled, ok := s.Enabled[models.EnvFeature{
EnvID: env.ID,
FeatureID: feature.ID,
}]
fs.Status[env.Name] = ok && enabled
}

if err := r.store.UpdateFeature(feature); err != nil {
return InternalServerError(c, err)
}

for envName, enabled := range feature.Status {
if prevStats[envName] != enabled {
go func(featureName string, enabled bool, envName string) {
if err := r.notifier.NotifyStatusChange(featureName, enabled, envName); err != nil {
log.Error("failed to notify feature status change: ", err)
}
}(feature.Name, enabled, envName)
}
}
return &fs
}

return OK(c, feature)
type featureStatus struct {
models.Feature
Status map[string]bool `json:"status"`
}
12 changes: 9 additions & 3 deletions api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,18 @@ func AuthMiddleware(rootUsername, rootPassword string, s store.Store) echo.Middl
return password == rootPassword
}

user, err := s.GetUserByUsername(username)
state, err := s.State()
if err != nil {
log.Error("Failed to retrieve user: ", err)
log.Error("Failed to get state: ", err)
return false
}

return bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) == nil
for _, user := range state.Users {
if user.Username == username {
return bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) == nil
}
}

return false
})
}
12 changes: 3 additions & 9 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,16 @@ func NewServer(conf ServerConfig) (*standard.Server, error) {

features := NewFeatureResource(conf.Store, conf.Stats, conf.Notifier)
environments := NewEnvironmentResource(conf.Store, conf.Stats)
users := NewUserResource(conf.Store, conf.Stats)
events := NewEventResource(conf.Store, conf.Stats, conf.Notifier)

e.Get("/api/health", standard.WrapHandler(healthz.Handler()))

e.Post("/api/events/:type", events.Create, basicAuthMiddleware)

e.Get("/api/features/:name", features.Get, basicAuthMiddleware)
e.Get("/api/features", features.List, basicAuthMiddleware)
e.Post("/api/features", features.Create, basicAuthMiddleware)
e.Patch("/api/features/:name", features.Update, basicAuthMiddleware)

e.Get("/api/environments/:name", environments.Get, basicAuthMiddleware)
e.Get("/api/environments", environments.List, basicAuthMiddleware)
e.Post("/api/environments", environments.Create, basicAuthMiddleware)
e.Patch("/api/environments/:name", environments.Update, basicAuthMiddleware)

e.Get("/api/users/:username", users.Get, basicAuthMiddleware)
e.Post("/api/users", users.Create, basicAuthMiddleware)

e.Static("/static", "public")
e.File("/*", "public/index.html")
Expand Down
Loading

0 comments on commit e347004

Please sign in to comment.