All files / web/src/app/api/debug/cleanup route.ts

0% Statements 0/142
0% Branches 0/1
0% Functions 0/1
0% Lines 0/142

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143                                                                                                                                                                                                                                                                                             
import { and, eq, inArray } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db, schema } from '@/db'
import { withAuth } from '@/lib/auth/withAuth'
import { seedProfilePlayers } from '@/db/schema/seed-profile-players'
import { getUserId } from '@/lib/viewer'

/**
 * Find all expungeable players owned by the current user.
 *
 * Players are expungeable when `is_expungeable = true`, which is set at
 * creation time by debug endpoints, seed tools, and e2e tests.
 *
 * As a fallback for players created before the flag existed, also matches:
 * - Debug-{timestamp} name + 🐛 emoji (debug hub)
 * - * Test Child name suffix (e2e tests)
 * - Entries in seed_profile_players (seed tool)
 *
 * Only returns players owned by the requesting user (via parent_child).
 */
async function findCleanupCandidates(userId: string) {
  // Get all player IDs owned by this user
  const ownedRelations = await db
    .select({ playerId: schema.parentChild.childPlayerId })
    .from(schema.parentChild)
    .where(eq(schema.parentChild.parentUserId, userId))

  const ownedIds = ownedRelations.map((r) => r.playerId)
  if (ownedIds.length === 0) return []

  // Find seed players (tracked in seed_profile_players) for source annotation
  const seedEntries = await db
    .select({ playerId: seedProfilePlayers.playerId, profileId: seedProfilePlayers.profileId })
    .from(seedProfilePlayers)
    .where(inArray(seedProfilePlayers.playerId, ownedIds))

  const seedPlayerIds = new Set(seedEntries.map((e) => e.playerId))

  // Fetch all owned players
  const allOwned = await db
    .select({
      id: schema.players.id,
      name: schema.players.name,
      emoji: schema.players.emoji,
      color: schema.players.color,
      createdAt: schema.players.createdAt,
      isExpungeable: schema.players.isExpungeable,
    })
    .from(schema.players)
    .where(inArray(schema.players.id, ownedIds))

  const candidates = allOwned.filter((p) => {
    // Primary: flag set at creation time
    if (p.isExpungeable) return true
    // Fallback: legacy patterns for players created before the flag existed
    if (seedPlayerIds.has(p.id)) return true
    if (p.name.startsWith('Debug-') && p.emoji === '🐛') return true
    if (p.name.endsWith(' Test Child')) return true
    return false
  })

  // Annotate with source
  return candidates.map((p) => {
    const seedEntry = seedEntries.find((e) => e.playerId === p.id)
    let source: string = 'debug'
    if (seedEntry) source = `seed:${seedEntry.profileId}`
    else if (p.name.endsWith(' Test Child')) source = 'e2e'
    return {
      id: p.id,
      name: p.name,
      emoji: p.emoji,
      color: p.color,
      createdAt: p.createdAt,
      source,
    }
  })
}

/**
 * GET /api/debug/cleanup
 *
 * Preview: returns the list of expungeable players that would be deleted.
 */
export const GET = withAuth(
  async () => {
    try {
      const userId = await getUserId()
      const candidates = await findCleanupCandidates(userId)

      return NextResponse.json({
        players: candidates,
        count: candidates.length,
      })
    } catch (error) {
      console.error('[debug/cleanup] Preview failed:', error)
      return NextResponse.json(
        { error: error instanceof Error ? error.message : 'Failed to preview cleanup' },
        { status: 500 }
      )
    }
  },
  { role: 'admin' }
)

/**
 * DELETE /api/debug/cleanup
 *
 * Deletes all expungeable players owned by the current user.
 * All related data (sessions, skills, enrollments, etc.) cascades automatically.
 */
export const DELETE = withAuth(
  async () => {
    try {
      const userId = await getUserId()
      const candidates = await findCleanupCandidates(userId)

      if (candidates.length === 0) {
        return NextResponse.json({ deleted: 0, players: [] })
      }

      const ids = candidates.map((c) => c.id)

      // Delete seed_profile_players entries first (FK references players)
      await db.delete(seedProfilePlayers).where(inArray(seedProfilePlayers.playerId, ids))

      // Delete the players (cascades to all related tables)
      await db.delete(schema.players).where(inArray(schema.players.id, ids))

      return NextResponse.json({
        deleted: candidates.length,
        players: candidates.map((c) => ({ id: c.id, name: c.name, source: c.source })),
      })
    } catch (error) {
      console.error('[debug/cleanup] Delete failed:', error)
      return NextResponse.json(
        { error: error instanceof Error ? error.message : 'Failed to cleanup' },
        { status: 500 }
      )
    }
  },
  { role: 'admin' }
)