Files
core/internal/history/cross_entity.go

222 lines
7.3 KiB
Go

package history
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"reanimator/internal/idgen"
)
type InstallationChangeResult struct {
Changed bool
}
func (s *Service) InstallComponentToAssetWithTx(ctx context.Context, tx *sql.Tx, componentID, assetID string, installedAt time.Time, sourceType string, sourceRef, correlationID, idempotencyKey *string) (InstallationChangeResult, error) {
if tx == nil {
return InstallationChangeResult{}, fmt.Errorf("%w: tx is required", ErrInvalidPatch)
}
if strings.TrimSpace(componentID) == "" || strings.TrimSpace(assetID) == "" {
return InstallationChangeResult{}, fmt.Errorf("%w: component_id and asset_id required", ErrInvalidPatch)
}
when := installedAt.UTC()
componentState, err := s.loadComponentStateForUpdate(ctx, tx, componentID)
if err != nil {
return InstallationChangeResult{}, err
}
assetState, err := s.loadAssetStateForUpdate(ctx, tx, assetID)
if err != nil {
return InstallationChangeResult{}, err
}
if componentState.Installation.CurrentMachineID != nil && *componentState.Installation.CurrentMachineID == assetID {
return InstallationChangeResult{Changed: false}, nil
}
_ = assetState
effText := when.Format(time.RFC3339Nano)
compPatch := []PatchOp{
{Op: "replace", Path: "/installation/current_machine_id", Value: assetID},
{Op: "replace", Path: "/installation/installed_at", Value: effText},
}
compRes, err := s.ApplyComponentPatchWithTx(ctx, tx, ApplyPatchCommand{
EntityID: componentID,
ChangeType: "COMPONENT_INSTALLED",
EffectiveAt: &when,
SourceType: sourceType,
SourceRef: sourceRef,
ActorType: "ingest",
CorrelationID: correlationID,
IdempotencyKey: idempotencyKey,
Patch: compPatch,
})
if err != nil {
return InstallationChangeResult{}, err
}
if compRes.Duplicate {
return InstallationChangeResult{Changed: false}, nil
}
updatedAsset, err := s.loadAssetStateForUpdate(ctx, tx, assetID)
if err != nil {
return InstallationChangeResult{}, err
}
if !containsString(updatedAsset.Inventory.InstalledComponentIDs, componentID) {
updatedAsset.Inventory.InstalledComponentIDs = append(updatedAsset.Inventory.InstalledComponentIDs, componentID)
updatedAsset.Inventory.InstalledComponentIDs = sortedStrings(updatedAsset.Inventory.InstalledComponentIDs)
}
assetPatch, err := diffAssetStates(assetState, updatedAsset)
if err != nil {
return InstallationChangeResult{}, err
}
if len(assetPatch) > 0 {
assetIdem := deriveChildIdem(idempotencyKey, "asset")
if _, err := s.ApplyAssetPatchWithTx(ctx, tx, ApplyPatchCommand{
EntityID: assetID,
ChangeType: "ASSET_COMPONENT_SET_CHANGED",
EffectiveAt: &when,
SourceType: sourceType,
SourceRef: sourceRef,
ActorType: "ingest",
CorrelationID: correlationID,
IdempotencyKey: assetIdem,
Patch: assetPatch,
}); err != nil {
return InstallationChangeResult{}, err
}
}
if err := s.upsertInstallationProjectionTx(ctx, tx, componentID, assetID, when); err != nil {
return InstallationChangeResult{}, err
}
return InstallationChangeResult{Changed: true}, nil
}
func (s *Service) RemoveComponentFromAssetWithTx(ctx context.Context, tx *sql.Tx, componentID, assetID string, removedAt time.Time, sourceType string, sourceRef, correlationID, idempotencyKey *string) (InstallationChangeResult, error) {
if tx == nil {
return InstallationChangeResult{}, fmt.Errorf("%w: tx is required", ErrInvalidPatch)
}
when := removedAt.UTC()
componentState, err := s.loadComponentStateForUpdate(ctx, tx, componentID)
if err != nil {
return InstallationChangeResult{}, err
}
if componentState.Installation.CurrentMachineID == nil || *componentState.Installation.CurrentMachineID != assetID {
return InstallationChangeResult{Changed: false}, nil
}
assetState, err := s.loadAssetStateForUpdate(ctx, tx, assetID)
if err != nil {
return InstallationChangeResult{}, err
}
compPatch := []PatchOp{
{Op: "remove", Path: "/installation/current_machine_id"},
{Op: "remove", Path: "/installation/installed_at"},
}
compRes, err := s.ApplyComponentPatchWithTx(ctx, tx, ApplyPatchCommand{
EntityID: componentID,
ChangeType: "COMPONENT_REMOVED",
EffectiveAt: &when,
SourceType: sourceType,
SourceRef: sourceRef,
ActorType: "ingest",
CorrelationID: correlationID,
IdempotencyKey: idempotencyKey,
Patch: compPatch,
})
if err != nil {
return InstallationChangeResult{}, err
}
if compRes.Duplicate {
return InstallationChangeResult{Changed: false}, nil
}
updatedAsset := assetState
updatedAsset.Inventory.InstalledComponentIDs = removeString(updatedAsset.Inventory.InstalledComponentIDs, componentID)
updatedAsset.Inventory.InstalledComponentIDs = sortedStrings(updatedAsset.Inventory.InstalledComponentIDs)
assetPatch, err := diffAssetStates(assetState, updatedAsset)
if err != nil {
return InstallationChangeResult{}, err
}
if len(assetPatch) > 0 {
assetIdem := deriveChildIdem(idempotencyKey, "asset")
if _, err := s.ApplyAssetPatchWithTx(ctx, tx, ApplyPatchCommand{
EntityID: assetID,
ChangeType: "ASSET_COMPONENT_SET_CHANGED",
EffectiveAt: &when,
SourceType: sourceType,
SourceRef: sourceRef,
ActorType: "ingest",
CorrelationID: correlationID,
IdempotencyKey: assetIdem,
Patch: assetPatch,
}); err != nil {
return InstallationChangeResult{}, err
}
}
if err := s.closeInstallationProjectionTx(ctx, tx, componentID, assetID, when); err != nil {
return InstallationChangeResult{}, err
}
return InstallationChangeResult{Changed: true}, nil
}
func (s *Service) upsertInstallationProjectionTx(ctx context.Context, tx *sql.Tx, componentID, assetID string, installedAt time.Time) error {
if _, err := tx.ExecContext(ctx, `
UPDATE installations
SET removed_at = ?
WHERE part_id = ? AND removed_at IS NULL AND machine_id <> ?`,
installedAt, componentID, assetID,
); err != nil {
return err
}
var exists int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM installations WHERE part_id = ? AND machine_id = ? AND removed_at IS NULL`, componentID, assetID).Scan(&exists); err != nil {
return err
}
if exists > 0 {
return nil
}
installationID, err := s.generateID(ctx, tx, idgen.Installation)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO installations (id, machine_id, part_id, installed_at) VALUES (?, ?, ?, ?)`, installationID, assetID, componentID, installedAt)
return err
}
func (s *Service) closeInstallationProjectionTx(ctx context.Context, tx *sql.Tx, componentID, assetID string, removedAt time.Time) error {
_, err := tx.ExecContext(ctx, `
UPDATE installations
SET removed_at = ?
WHERE machine_id = ? AND part_id = ? AND removed_at IS NULL`,
removedAt, assetID, componentID,
)
return err
}
func deriveChildIdem(base *string, suffix string) *string {
if base == nil || strings.TrimSpace(*base) == "" {
return nil
}
value := strings.TrimSpace(*base) + ":" + suffix
return &value
}
func containsString(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
func removeString(items []string, target string) []string {
out := make([]string, 0, len(items))
for _, item := range items {
if item == target {
continue
}
out = append(out, item)
}
return out
}