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
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
208 lines
4.8 KiB
Go
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{}
|