// Package moduleregistry implementiert Core API-02: die Registry, in der // sich Fachmodule (DMS, Mail, weitere) mit Metadaten eintragen, gekoppelt an // die Aktivierungspruefung aus LIC-02 (Feature-Flags). Zusaetzlich // authentifiziert die Registry Modul-Instanzen selbst ueber ein bei // Provisionierung ausgestelltes Service-Credential. package moduleregistry import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/flag" ) var ( ErrMissingName = errors.New("moduleregistry: name darf nicht leer sein") ErrMissingVersion = errors.New("moduleregistry: version darf nicht leer sein") ErrModuleNotFound = errors.New("moduleregistry: modul nicht registriert") ) type Module struct { Name string Version string RequiredFlags []string } type Registry struct { pool *pgxpool.Pool flags *flag.Service } func NewRegistry(pool *pgxpool.Pool, flags *flag.Service) *Registry { return &Registry{pool: pool, flags: flags} } // Register traegt ein Modul mit Name, Version und benoetigten Feature-Flags // ein (Akzeptanzkriterium 1). Fehlende Pflichtangaben werden abgewiesen // (Akzeptanzkriterium 1 / Pruefung 2). Erneutes Register desselben Namens // aktualisiert Version/Flags (Redeploy-Fall). func (r *Registry) Register(ctx context.Context, name, version string, requiredFlags []string) (Module, error) { if name == "" { return Module{}, ErrMissingName } if version == "" { return Module{}, ErrMissingVersion } if requiredFlags == nil { requiredFlags = []string{} } _, err := r.pool.Exec(ctx, ` INSERT INTO modules (name, version, required_flags, registered_at) VALUES ($1, $2, $3, now()) ON CONFLICT (name) DO UPDATE SET version = $2, required_flags = $3, registered_at = now() `, name, version, requiredFlags) if err != nil { return Module{}, fmt.Errorf("modul registrieren: %w", err) } return Module{Name: name, Version: version, RequiredFlags: requiredFlags}, nil } func (r *Registry) Get(ctx context.Context, name string) (Module, error) { var m Module m.Name = name err := r.pool.QueryRow(ctx, ` SELECT version, required_flags FROM modules WHERE name = $1 `, name).Scan(&m.Version, &m.RequiredFlags) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return Module{}, ErrModuleNotFound } return Module{}, fmt.Errorf("modul lesen: %w", err) } return m, nil } // List liefert alle registrierten Module (Akzeptanzkriterium 3: ueber API // abfragbar, z.B. fuer Statusseite/Lizenzoberflaeche). func (r *Registry) List(ctx context.Context) ([]Module, error) { rows, err := r.pool.Query(ctx, `SELECT name, version, required_flags FROM modules ORDER BY name`) if err != nil { return nil, fmt.Errorf("module auflisten: %w", err) } defer rows.Close() var out []Module for rows.Next() { var m Module if err := rows.Scan(&m.Name, &m.Version, &m.RequiredFlags); err != nil { return nil, fmt.Errorf("modul lesen: %w", err) } out = append(out, m) } return out, rows.Err() } // IsActive prueft, ob ein registriertes Modul fuer einen Tenant aktiviert // ist: registriert UND alle benoetigten Feature-Flags sind fuer diesen // Tenant aktiv (Akzeptanzkriterium 2). Ein nicht registriertes Modul gilt // immer als nicht aktiv. func (r *Registry) IsActive(ctx context.Context, tenantSlug, moduleName string) (bool, error) { m, err := r.Get(ctx, moduleName) if err != nil { if errors.Is(err, ErrModuleNotFound) { return false, nil } return false, err } for _, flagKey := range m.RequiredFlags { if !r.flags.IsEnabled(ctx, tenantSlug, flagKey) { return false, nil } } return true, nil }