301 lines
8.3 KiB
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
|
|
}
|