Files
creator-hub/internal/creator/deletion.go
T

289 lines
9.8 KiB
Go

package creator
import (
"context"
"database/sql"
"errors"
"time"
)
func (s *Store) CheckOwnedAccountDeletion(ctx context.Context, accountID string) error {
if accountID == "" {
return ErrInvalid
}
var exists bool
if err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM social_account WHERE id = $1)`, accountID).Scan(&exists); err != nil {
return databaseError(err)
}
if !exists {
return ErrNotFound
}
var active bool
if err := s.db.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1 FROM creator_operation
WHERE account_id = $1 AND state = 'processing'
UNION ALL
SELECT 1 FROM creator_event
WHERE (receiving_account_id = $1 OR execution_account_id = $1) AND state = 'processing'
UNION ALL
SELECT 1 FROM creator_collection_checkpoint
WHERE source_type = 'owned' AND source_id = $1 AND status = 'running'
UNION ALL
SELECT 1 FROM creator_source_sync_lease
WHERE source_type = 'owned' AND source_id = $1 AND lease_until > now()
)`, accountID).Scan(&active); err != nil {
return databaseError(err)
}
if active {
return ErrConflict
}
return nil
}
func (s *Store) DeleteOwnedAccountData(ctx context.Context, accountID string) error {
if accountID == "" {
return ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return errors.New("begin creator account deletion")
}
defer tx.Rollback()
if err := lockCreatorAccount(ctx, tx, accountID); err != nil {
return err
}
if err := checkOwnedAccountDeletionTx(ctx, tx, accountID); err != nil {
return err
}
var secretID, secretKey string
var hasSecret bool
if err := tx.QueryRowContext(ctx, `
SELECT secret_reference_id, secret_key
FROM creator_account_password
WHERE account_id = $1`, accountID).Scan(&secretID, &secretKey); err == nil {
hasSecret = true
} else if !errors.Is(err, sql.ErrNoRows) {
return databaseError(err)
}
if hasSecret && s.secrets == nil {
return errors.New("creator secret bridge is unavailable")
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_operation
WHERE account_id = $1
OR strategy_id IN (SELECT id FROM creator_strategy WHERE big_account_id = $1 OR execution_account_id = $1)
OR event_id IN (SELECT id FROM creator_event WHERE receiving_account_id = $1 OR execution_account_id = $1)`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_event
WHERE receiving_account_id = $1 OR execution_account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_cooldown
WHERE big_account_id = $1 OR execution_account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_strategy
WHERE big_account_id = $1 OR execution_account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_relation
WHERE big_account_id = $1 OR small_account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_conversation WHERE account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_listener_boundary WHERE account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_listener_state WHERE account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if err := deleteCreatorSourceTx(ctx, tx, SourceOwned, accountID); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_password WHERE account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_profile WHERE account_id = $1`, accountID); err != nil {
return databaseError(err)
}
if err := tx.Commit(); err != nil {
return errors.New("commit creator account deletion")
}
if hasSecret {
if err := s.secrets.Delete(context.WithoutCancel(ctx), SecretReference{ID: secretID, Provider: "os_keyring"}, secretKey); err != nil {
return errors.Join(errors.New("delete creator account password"), err)
}
}
return nil
}
func (s *Store) DeleteCompetitor(ctx context.Context, competitorID string) error {
if competitorID == "" {
return ErrInvalid
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return errors.New("begin competitor deletion")
}
defer tx.Rollback()
var syncStatus string
var syncLease sql.NullTime
if err := tx.QueryRowContext(ctx, `
SELECT sync_status, sync_lease_until
FROM creator_competitor
WHERE id = $1
FOR UPDATE`, competitorID).Scan(&syncStatus, &syncLease); err != nil {
return rowError(err)
}
if syncStatus == "running" || (syncLease.Valid && syncLease.Time.After(time.Now())) {
return ErrConflict
}
var active bool
if err := tx.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1 FROM creator_collection_checkpoint
WHERE source_type = 'competitor' AND source_id = $1
AND (status = 'running' OR (lease_until IS NOT NULL AND lease_until > now()))
UNION ALL
SELECT 1 FROM creator_source_sync_lease
WHERE source_type = 'competitor' AND source_id = $1 AND lease_until > now()
)`, competitorID).Scan(&active); err != nil {
return databaseError(err)
}
if active {
return ErrConflict
}
if err := deleteCreatorSourceTx(ctx, tx, SourceCompetitor, competitorID); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_competitor WHERE id = $1`, competitorID); err != nil {
return databaseError(err)
}
if err := tx.Commit(); err != nil {
return errors.New("commit competitor deletion")
}
return nil
}
func lockCreatorAccount(ctx context.Context, tx *sql.Tx, accountID string) error {
var lockedID string
if err := tx.QueryRowContext(ctx, `SELECT id FROM social_account WHERE id = $1 FOR UPDATE`, accountID).Scan(&lockedID); err != nil {
return rowError(err)
}
return nil
}
func checkOwnedAccountDeletionTx(ctx context.Context, tx *sql.Tx, accountID string) error {
var active bool
if err := tx.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1 FROM creator_operation
WHERE account_id = $1 AND state = 'processing'
UNION ALL
SELECT 1 FROM creator_event
WHERE (receiving_account_id = $1 OR execution_account_id = $1) AND state = 'processing'
UNION ALL
SELECT 1 FROM creator_collection_checkpoint
WHERE source_type = 'owned' AND source_id = $1 AND status = 'running'
UNION ALL
SELECT 1 FROM creator_source_sync_lease
WHERE source_type = 'owned' AND source_id = $1 AND lease_until > now()
)`, accountID).Scan(&active); err != nil {
return databaseError(err)
}
if active {
return ErrConflict
}
return nil
}
func deleteCreatorSourceTx(ctx context.Context, tx *sql.Tx, sourceType, sourceID string) error {
if _, err := tx.ExecContext(ctx, `
CREATE TEMP TABLE creator_delete_work_ids (id text PRIMARY KEY) ON COMMIT DROP`); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO creator_delete_work_ids (id)
SELECT id FROM creator_work WHERE source_type = $1 AND source_id = $2
UNION
SELECT work_id FROM creator_work_source WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil {
return databaseError(err)
}
var active bool
if err := tx.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1 FROM creator_operation
WHERE state = 'processing'
AND (target_work_id IN (SELECT id FROM creator_delete_work_ids)
OR target_comment_id IN (SELECT id FROM creator_comment WHERE work_id IN (SELECT id FROM creator_delete_work_ids)))
UNION ALL
SELECT 1 FROM creator_event
WHERE state = 'processing' AND work_id IN (SELECT id FROM creator_delete_work_ids)
)`).Scan(&active); err != nil {
return databaseError(err)
}
if active {
return ErrConflict
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_work_source WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_source_sync_lease WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `DELETE FROM creator_collection_checkpoint WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
UPDATE creator_work work
SET source_type = source.source_type, source_id = source.source_id
FROM (
SELECT DISTINCT ON (work_id) work_id, source_type, source_id
FROM creator_work_source
ORDER BY work_id, created_at, source_type, source_id
) source
WHERE work.id = source.work_id AND work.source_type = $1 AND work.source_id = $2`, sourceType, sourceID); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_operation
WHERE target_work_id IN (
SELECT id FROM creator_delete_work_ids
WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id)
)
OR target_comment_id IN (
SELECT comment.id FROM creator_comment comment
WHERE comment.work_id IN (
SELECT id FROM creator_delete_work_ids
WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id)
)
)`); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_event
WHERE work_id IN (
SELECT id FROM creator_delete_work_ids
WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id)
)`); err != nil {
return databaseError(err)
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM creator_work
WHERE id IN (
SELECT id FROM creator_delete_work_ids
WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id)
)`); err != nil {
return databaseError(err)
}
return nil
}