nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
Raw
History Blame Contribute Delete
7.27 kB
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { getDatabase } from '@/lib/db'
import { heavyLimiter } from '@/lib/rate-limit'
interface SearchResult {
type: 'task' | 'agent' | 'activity' | 'audit' | 'message' | 'notification' | 'webhook' | 'pipeline'
id: number
title: string
subtitle?: string
excerpt?: string
created_at: number
relevance: number
}
/**
* GET /api/search?q=<query>&type=<optional type filter>&limit=<optional>
* Global search across all MC entities.
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = heavyLimiter(request)
if (rateCheck) return rateCheck
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')?.trim()
const typeFilter = searchParams.get('type')
const limit = Math.min(parseInt(searchParams.get('limit') || '30'), 100)
if (!query || query.length < 2) {
return NextResponse.json({ error: 'Query must be at least 2 characters' }, { status: 400 })
}
const db = getDatabase()
const workspaceId = auth.user.workspace_id ?? 1
const likeQ = `%${query}%`
const results: SearchResult[] = []
// Search tasks
if (!typeFilter || typeFilter === 'task') {
try {
const tasks = db.prepare(`
SELECT id, title, description, status, assigned_to, created_at
FROM tasks WHERE workspace_id = ? AND (title LIKE ? OR description LIKE ? OR assigned_to LIKE ?)
ORDER BY created_at DESC LIMIT ?
`).all(workspaceId, likeQ, likeQ, likeQ, limit) as any[]
for (const t of tasks) {
results.push({
type: 'task',
id: t.id,
title: t.title,
subtitle: `${t.status} ${t.assigned_to ? ${t.assigned_to}` : ''}`,
excerpt: truncateMatch(t.description, query),
created_at: t.created_at,
relevance: t.title.toLowerCase().includes(query.toLowerCase()) ? 2 : 1,
})
}
} catch { /* table might not exist */ }
}
// Search agents
if (!typeFilter || typeFilter === 'agent') {
try {
const agents = db.prepare(`
SELECT id, name, role, status, last_activity, created_at
FROM agents WHERE workspace_id = ? AND (name LIKE ? OR role LIKE ? OR last_activity LIKE ?)
ORDER BY created_at DESC LIMIT ?
`).all(workspaceId, likeQ, likeQ, likeQ, limit) as any[]
for (const a of agents) {
results.push({
type: 'agent',
id: a.id,
title: a.name,
subtitle: `${a.role} · ${a.status}`,
excerpt: a.last_activity,
created_at: a.created_at,
relevance: a.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1,
})
}
} catch { /* table might not exist */ }
}
// Search activities
if (!typeFilter || typeFilter === 'activity') {
try {
const activities = db.prepare(`
SELECT id, type, actor, description, created_at
FROM activities WHERE workspace_id = ? AND (description LIKE ? OR actor LIKE ?)
ORDER BY created_at DESC LIMIT ?
`).all(workspaceId, likeQ, likeQ, limit) as any[]
for (const a of activities) {
results.push({
type: 'activity',
id: a.id,
title: a.description,
subtitle: `by ${a.actor}`,
created_at: a.created_at,
relevance: 1,
})
}
} catch { /* table might not exist */ }
}
// Search audit log (admin-only — audit_log is instance-global)
if ((!typeFilter || typeFilter === 'audit') && auth.user.role === 'admin') {
try {
const audits = db.prepare(`
SELECT id, action, actor, detail, created_at
FROM audit_log WHERE action LIKE ? OR actor LIKE ? OR detail LIKE ?
ORDER BY created_at DESC LIMIT ?
`).all(likeQ, likeQ, likeQ, limit) as any[]
for (const a of audits) {
results.push({
type: 'audit',
id: a.id,
title: a.action,
subtitle: `by ${a.actor}`,
excerpt: truncateMatch(a.detail, query),
created_at: a.created_at,
relevance: 1,
})
}
} catch { /* table might not exist */ }
}
// Search messages
if (!typeFilter || typeFilter === 'message') {
try {
const messages = db.prepare(`
SELECT id, from_agent, to_agent, content, conversation_id, created_at
FROM messages WHERE workspace_id = ? AND (content LIKE ? OR from_agent LIKE ?)
ORDER BY created_at DESC LIMIT ?
`).all(workspaceId, likeQ, likeQ, limit) as any[]
for (const m of messages) {
results.push({
type: 'message',
id: m.id,
title: `${m.from_agent}${m.to_agent || 'all'}`,
subtitle: m.conversation_id,
excerpt: truncateMatch(m.content, query),
created_at: m.created_at,
relevance: 1,
})
}
} catch { /* table might not exist */ }
}
// Search webhooks
if (!typeFilter || typeFilter === 'webhook') {
try {
const webhooks = db.prepare(`
SELECT id, name, url, events, created_at
FROM webhooks WHERE workspace_id = ? AND (name LIKE ? OR url LIKE ?)
ORDER BY created_at DESC LIMIT ?
`).all(workspaceId, likeQ, likeQ, limit) as any[]
for (const w of webhooks) {
results.push({
type: 'webhook',
id: w.id,
title: w.name,
subtitle: w.url,
created_at: w.created_at,
relevance: w.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1,
})
}
} catch { /* table might not exist */ }
}
// Search pipelines
if (!typeFilter || typeFilter === 'pipeline') {
try {
const pipelines = db.prepare(`
SELECT id, name, description, created_at
FROM workflow_pipelines WHERE workspace_id = ? AND (name LIKE ? OR description LIKE ?)
ORDER BY created_at DESC LIMIT ?
`).all(workspaceId, likeQ, likeQ, limit) as any[]
for (const p of pipelines) {
results.push({
type: 'pipeline',
id: p.id,
title: p.name,
excerpt: truncateMatch(p.description, query),
created_at: p.created_at,
relevance: p.name.toLowerCase().includes(query.toLowerCase()) ? 2 : 1,
})
}
} catch { /* table might not exist */ }
}
// Sort by relevance then recency
results.sort((a, b) => b.relevance - a.relevance || b.created_at - a.created_at)
return NextResponse.json({
query,
count: results.length,
results: results.slice(0, limit),
})
}
function truncateMatch(text: string | null, query: string, maxLen = 120): string | undefined {
if (!text) return undefined
const lower = text.toLowerCase()
const idx = lower.indexOf(query.toLowerCase())
if (idx === -1) return text.slice(0, maxLen) + (text.length > maxLen ? '...' : '')
const start = Math.max(0, idx - 40)
const end = Math.min(text.length, idx + query.length + 80)
const excerpt = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '')
return excerpt
}