Files
schemas/domain/commands.go
T
argoyleandClaude Opus 5 57682f2dc3
schemas / check (push) Skipped
schemas / vulnerabilities (push) Skipped
schemas / check-release (push) Skipped
schemas / build (push) Skipped
pre-commit / pre-commit (push) Skipped
schemas / vulnerabilities (pull_request) Successful in 58s
schemas / check-release (pull_request) Successful in 59s
schemas / check (pull_request) Successful in 1m58s
pre-commit / pre-commit (pull_request) Successful in 2m54s
schemas / build (pull_request) Successful in 6m9s
schemas / deploy-prod (pull_request) Skipped
refactor: migrate eventsourced to codeberg and go-messaging-amqp
Move eventsourced modules from gitlab.com/unboundsoftware to codeberg.org/eventsourced.
The Codeberg amqp publisher is built on go-messaging-amqp, so the service
switches from sparetimecoders/goamqp as well:

- routing keys are mapped on the eventsourced publisher (newEventPublisher)
- cache consumers are typed handlers; Cache.Update drops the goamqp shape
- publishes detach from the request context (uncancelledPublisher)
- closeEvents is buffered and the receiver also watches rootCtx
- WithPrefetchLimit dropped (no-op, 20 is the default)
- amqp091-go v1.15.0 (GO-2026-6372)
- renovate groups codeberg.org/messaging with eventsourced

Only transient consumer queues are used, so no queue cutover is needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpb7Y47dPz246oY9bCzh7k
2026-09-16 17:23:38 +02:00

208 lines
4.8 KiB
Go

package domain
import (
"context"
"fmt"
"strings"
"codeberg.org/eventsourced/eventsourced"
"gitea.unbound.se/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{}