Files
core/internal/repository/registry/machines.go

301 lines
8.3 KiB
Go

package registry
import (
"context"
"database/sql"
"strings"
"reanimator/internal/domain"
"reanimator/internal/idgen"
)
type AssetRepository struct {
db *sql.DB
idgen *idgen.Generator
}
func NewAssetRepository(db *sql.DB) *AssetRepository {
return &AssetRepository{
db: db,
idgen: idgen.NewGenerator(db),
}
}
func (r *AssetRepository) Create(ctx context.Context, asset domain.Asset) (domain.Asset, error) {
id, err := r.idgen.Generate(ctx, idgen.Asset)
if err != nil {
return domain.Asset{}, err
}
_, err = r.db.ExecContext(ctx,
`INSERT INTO machines (id, name, vendor, model, vendor_serial, machine_tag)
VALUES (?, ?, ?, ?, ?, ?)`,
id,
asset.Name,
asset.Vendor,
asset.Model,
asset.VendorSerial,
asset.MachineTag,
)
if err != nil {
return domain.Asset{}, classifyError(err)
}
return r.Get(ctx, id)
}
func (r *AssetRepository) Get(ctx context.Context, id string) (domain.Asset, error) {
var asset domain.Asset
var vendor sql.NullString
var model sql.NullString
var machineTag sql.NullString
row := r.db.QueryRowContext(ctx,
`SELECT id, name, vendor, model, vendor_serial, machine_tag, created_at, updated_at
FROM machines WHERE id = ?`,
id,
)
if err := row.Scan(&asset.ID, &asset.Name, &vendor, &model, &asset.VendorSerial, &machineTag, &asset.CreatedAt, &asset.UpdatedAt); err != nil {
if err == sql.ErrNoRows {
return domain.Asset{}, ErrNotFound
}
return domain.Asset{}, err
}
asset.Vendor = nullStringToPtr(vendor)
asset.Model = nullStringToPtr(model)
asset.MachineTag = nullStringToPtr(machineTag)
return asset, nil
}
func (r *AssetRepository) GetByVendorSerial(ctx context.Context, vendorSerial string) (domain.Asset, error) {
vendorSerial = strings.TrimSpace(vendorSerial)
if vendorSerial == "" {
return domain.Asset{}, ErrNotFound
}
var asset domain.Asset
var vendor sql.NullString
var model sql.NullString
var machineTag sql.NullString
row := r.db.QueryRowContext(ctx,
`SELECT id, name, vendor, model, vendor_serial, machine_tag, created_at, updated_at
FROM machines WHERE vendor_serial = ?`,
vendorSerial,
)
if err := row.Scan(&asset.ID, &asset.Name, &vendor, &model, &asset.VendorSerial, &machineTag, &asset.CreatedAt, &asset.UpdatedAt); err != nil {
if err == sql.ErrNoRows {
return domain.Asset{}, ErrNotFound
}
return domain.Asset{}, err
}
asset.Vendor = nullStringToPtr(vendor)
asset.Model = nullStringToPtr(model)
asset.MachineTag = nullStringToPtr(machineTag)
return asset, nil
}
func (r *AssetRepository) List(ctx context.Context) ([]domain.Asset, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT id, name, vendor, model, vendor_serial, machine_tag, created_at, updated_at
FROM machines ORDER BY created_at DESC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
machines := make([]domain.Asset, 0)
for rows.Next() {
var asset domain.Asset
var vendor sql.NullString
var model sql.NullString
var machineTag sql.NullString
if err := rows.Scan(&asset.ID, &asset.Name, &vendor, &model, &asset.VendorSerial, &machineTag, &asset.CreatedAt, &asset.UpdatedAt); err != nil {
return nil, err
}
asset.Vendor = nullStringToPtr(vendor)
asset.Model = nullStringToPtr(model)
asset.MachineTag = nullStringToPtr(machineTag)
machines = append(machines, asset)
}
if err := rows.Err(); err != nil {
return nil, err
}
return machines, nil
}
func (r *AssetRepository) Update(ctx context.Context, asset domain.Asset) (domain.Asset, error) {
result, err := r.db.ExecContext(ctx,
`UPDATE machines
SET name = ?, vendor = ?, model = ?, vendor_serial = ?, machine_tag = ?
WHERE id = ?`,
strings.TrimSpace(asset.Name),
asset.Vendor,
asset.Model,
strings.TrimSpace(asset.VendorSerial),
asset.MachineTag,
asset.ID,
)
if err != nil {
return domain.Asset{}, classifyError(err)
}
affected, err := result.RowsAffected()
if err != nil {
return domain.Asset{}, err
}
if affected == 0 {
return domain.Asset{}, ErrNotFound
}
return r.Get(ctx, asset.ID)
}
type AssetDeleteResult struct {
DeletedParts int
}
func (r *AssetRepository) DeleteWithDetails(ctx context.Context, assetID string) (AssetDeleteResult, error) {
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return AssetDeleteResult{}, err
}
defer func() { _ = tx.Rollback() }()
var exists int
if err := tx.QueryRowContext(ctx, `SELECT 1 FROM machines WHERE id = ? FOR UPDATE`, assetID).Scan(&exists); err != nil {
if err == sql.ErrNoRows {
return AssetDeleteResult{}, ErrNotFound
}
return AssetDeleteResult{}, err
}
partIDs, err := listAssetPartIDs(ctx, tx, assetID)
if err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM failure_events WHERE machine_id = ?`, assetID); err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM observations WHERE machine_id = ?`, assetID); err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM log_bundles WHERE machine_id = ?`, assetID); err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM installations WHERE machine_id = ?`, assetID); err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM machine_firmware_states WHERE machine_id = ?`, assetID); err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM timeline_events WHERE machine_id = ? OR (subject_type = 'asset' AND subject_id = ?)`, assetID, assetID); err != nil {
return AssetDeleteResult{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM machines WHERE id = ?`, assetID); err != nil {
return AssetDeleteResult{}, err
}
deletedPartIDs, err := deleteOrphanParts(ctx, tx, partIDs)
if err != nil {
return AssetDeleteResult{}, err
}
if err := tx.Commit(); err != nil {
return AssetDeleteResult{}, err
}
return AssetDeleteResult{DeletedParts: len(deletedPartIDs)}, nil
}
func listAssetPartIDs(ctx context.Context, tx *sql.Tx, assetID string) ([]string, error) {
rows, err := tx.QueryContext(ctx, `
SELECT DISTINCT part_id
FROM (
SELECT part_id FROM installations WHERE machine_id = ?
UNION ALL
SELECT part_id FROM observations WHERE machine_id = ?
UNION ALL
SELECT part_id FROM failure_events WHERE machine_id = ?
) linked
`, assetID, assetID, assetID)
if err != nil {
return nil, err
}
defer rows.Close()
ids := make([]string, 0)
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return ids, nil
}
func deleteOrphanParts(ctx context.Context, tx *sql.Tx, candidatePartIDs []string) ([]string, error) {
if len(candidatePartIDs) == 0 {
return nil, nil
}
placeholders := make([]string, 0, len(candidatePartIDs))
args := make([]any, 0, len(candidatePartIDs))
for _, id := range candidatePartIDs {
placeholders = append(placeholders, "?")
args = append(args, id)
}
query := `
SELECT p.id
FROM parts p
WHERE p.id IN (` + strings.Join(placeholders, ",") + `)
AND NOT EXISTS (SELECT 1 FROM installations i WHERE i.part_id = p.id)
AND NOT EXISTS (SELECT 1 FROM observations o WHERE o.part_id = p.id)
AND NOT EXISTS (SELECT 1 FROM failure_events f WHERE f.part_id = p.id)
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
deletable := make([]string, 0)
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
deletable = append(deletable, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(deletable) == 0 {
return nil, nil
}
placeholders = placeholders[:0]
args = args[:0]
for _, id := range deletable {
placeholders = append(placeholders, "?")
args = append(args, id)
}
timelineQuery := `DELETE FROM timeline_events WHERE part_id IN (` + strings.Join(placeholders, ",") + `) OR (subject_type = 'component' AND subject_id IN (` + strings.Join(placeholders, ",") + `))`
timelineArgs := append(append(make([]any, 0, len(args)*2), args...), args...)
if _, err := tx.ExecContext(ctx, timelineQuery, timelineArgs...); err != nil {
return nil, err
}
deleteQuery := `DELETE FROM parts WHERE id IN (` + strings.Join(placeholders, ",") + `)`
if _, err := tx.ExecContext(ctx, deleteQuery, args...); err != nil {
return nil, err
}
return deletable, nil
}