Files
schemas/domain/commands.go
T
argoyle ffcf41b85a feat: add commands for managing organizations and users
Introduce `AddUserToOrganization`, `RemoveAPIKey`, and 
`RemoveOrganization` commands to enhance organization 
management. Implement validation for user addition and 
API key removal. Update GraphQL schema to support new 
mutations and add caching for the new events, ensuring 
that organizations and their relationships are accurately 
represented in the cache.
2025-11-22 18:37:07 +01:00

208 lines
4.8 KiB
Go

package domain
import (
"context"
"fmt"
"strings"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/hash"
)
type AddOrganization struct {
Name string
Initiator string
}
func (a AddOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() != nil {
return fmt.Errorf("organization already exists")
}
if len(a.Name) == 0 {
return fmt.Errorf("name is required")
}
return nil
}
func (a AddOrganization) Event(context.Context) eventsourced.Event {
return &OrganizationAdded{
Name: a.Name,
Initiator: a.Initiator,
}
}
var _ eventsourced.Command = AddOrganization{}
type AddUserToOrganization struct {
UserId string
Initiator string
}
func (a AddUserToOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() == nil {
return fmt.Errorf("organization does not exist")
}
if len(a.UserId) == 0 {
return fmt.Errorf("userId is required")
}
// Check if user is already in the organization
org := aggregate.(*Organization)
for _, user := range org.Users {
if user == a.UserId {
return fmt.Errorf("user is already a member of this organization")
}
}
return nil
}
func (a AddUserToOrganization) Event(context.Context) eventsourced.Event {
return &UserAddedToOrganization{
UserId: a.UserId,
Initiator: a.Initiator,
}
}
var _ eventsourced.Command = AddUserToOrganization{}
type AddAPIKey struct {
Name string
Key string
Refs []string
Read bool
Publish bool
Initiator string
}
func (a AddAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() == nil {
return fmt.Errorf("organization does not exist")
}
for _, k := range aggregate.(*Organization).APIKeys {
if k.Name == a.Name {
return fmt.Errorf("a key named '%s' already exist", a.Name)
}
}
return nil
}
func (a AddAPIKey) Event(context.Context) eventsourced.Event {
// Hash the API key using bcrypt for secure storage
// Note: We can't return an error here, but bcrypt errors are extremely rare
// (only if system runs out of memory or bcrypt cost is invalid)
// We use a fixed cost of 12 which is always valid
hashedKey, err := hash.APIKey(a.Key)
if err != nil {
// This should never happen with bcrypt cost 12, but if it does,
// we'll store an empty hash which will fail validation later
hashedKey = ""
}
return &APIKeyAdded{
Name: a.Name,
Key: hashedKey,
Refs: a.Refs,
Read: a.Read,
Publish: a.Publish,
Initiator: a.Initiator,
}
}
var _ eventsourced.Command = AddAPIKey{}
type RemoveAPIKey struct {
KeyName string
Initiator string
}
func (r RemoveAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() == nil {
return fmt.Errorf("organization does not exist")
}
org := aggregate.(*Organization)
found := false
for _, k := range org.APIKeys {
if k.Name == r.KeyName {
found = true
break
}
}
if !found {
return fmt.Errorf("API key '%s' not found", r.KeyName)
}
return nil
}
func (r RemoveAPIKey) Event(context.Context) eventsourced.Event {
return &APIKeyRemoved{
KeyName: r.KeyName,
Initiator: r.Initiator,
}
}
var _ eventsourced.Command = RemoveAPIKey{}
type RemoveOrganization struct {
Initiator string
}
func (r RemoveOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
if aggregate.Identity() == nil {
return fmt.Errorf("organization does not exist")
}
return nil
}
func (r RemoveOrganization) Event(context.Context) eventsourced.Event {
return &OrganizationRemoved{
Initiator: r.Initiator,
}
}
var _ eventsourced.Command = RemoveOrganization{}
type UpdateSubGraph struct {
OrganizationId string
Ref string
Service string
Url *string
WSUrl *string
Sdl string
Initiator string
}
func (u UpdateSubGraph) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
switch a := aggregate.(type) {
case *SubGraph:
if strings.TrimSpace(u.Ref) == "" {
return fmt.Errorf("ref is missing")
}
if strings.TrimSpace(u.Service) == "" {
return fmt.Errorf("service is missing")
}
if strings.TrimSpace(u.Sdl) == "" {
return fmt.Errorf("SDL is missing")
}
if (u.Url == nil || strings.TrimSpace(*u.Url) == "") && a.Url == nil {
return fmt.Errorf("url is missing")
}
default:
return fmt.Errorf("aggregate is not a SubGraph")
}
return nil
}
func (u UpdateSubGraph) Event(context.Context) eventsourced.Event {
return &SubGraphUpdated{
OrganizationId: u.OrganizationId,
Ref: u.Ref,
Service: u.Service,
Url: u.Url,
WSUrl: u.WSUrl,
Sdl: u.Sdl,
Initiator: u.Initiator,
}
}
var _ eventsourced.Command = UpdateSubGraph{}