getMessage(), $e->status, $e->errorCode); } $clientId = (int)$tenant['client_id']; $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); $action = (string)($_GET['action'] ?? ($method === 'POST' ? '' : 'list')); $db = dbnToolsDb(); switch ($action) { case 'list': dbnToolsRequireMethod('GET'); respondList($db, $clientId); break; case 'get': dbnToolsRequireMethod('GET'); respondGet($db, $clientId); break; case 'update': dbnToolsRequireMethod('POST'); respondUpdate($db, $clientId); break; case 'delete': dbnToolsRequireMethod('POST'); respondDelete($db, $clientId); break; default: dbnToolsError('Unknown action.', 400, 'unknown_action'); } function respondList(PDO $db, int $clientId): void { $offset = max(0, (int)($_GET['offset'] ?? 0)); $limit = max(1, min(100, (int)($_GET['limit'] ?? 20))); $q = trim((string)($_GET['q'] ?? '')); $status = trim((string)($_GET['status'] ?? '')); $category = trim((string)($_GET['category'] ?? '')); $where = ['client_id = ?']; $params = [$clientId]; if ($q !== '') { $where[] = '(title LIKE ? OR tags LIKE ?)'; $like = '%' . str_replace(['%', '_'], ['\%', '\_'], $q) . '%'; $params[] = $like; $params[] = $like; } $allowedStatus = ['pending', 'processing', 'ready', 'error']; if ($status !== '' && in_array($status, $allowedStatus, true)) { $where[] = 'status = ?'; $params[] = $status; } if ($category !== '') { $where[] = 'category = ?'; $params[] = $category; } $sourceType = trim((string)($_GET['source_type'] ?? '')); $allowedSourceTypes = ['text', 'audio', 'url', 'tool-output', 'upload']; if ($sourceType !== '' && in_array($sourceType, $allowedSourceTypes, true)) { $where[] = 'source_type = ?'; $params[] = $sourceType; } $whereSql = 'WHERE ' . implode(' AND ', $where); $countStmt = $db->prepare("SELECT COUNT(*) FROM client_documents {$whereSql}"); $countStmt->execute($params); $total = (int)$countStmt->fetchColumn(); $sql = "SELECT id, title, source_type, language, category, tags, author, source_tool, import_method, status, word_count, chunk_count, file_size_bytes, source_url, error_message, created_at, updated_at FROM client_documents {$whereSql} ORDER BY id DESC LIMIT {$limit} OFFSET {$offset}"; $stmt = $db->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); dbnToolsRespond([ 'ok' => true, 'total' => $total, 'offset' => $offset, 'limit' => $limit, 'documents' => array_map('shapeDoc', $rows), ]); } function respondGet(PDO $db, int $clientId): void { $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { dbnToolsError('id is required.', 400, 'missing_id'); } $stmt = $db->prepare( 'SELECT * FROM client_documents WHERE id = ? AND client_id = ? LIMIT 1' ); $stmt->execute([$id, $clientId]); $doc = $stmt->fetch(PDO::FETCH_ASSOC); if (!$doc) { dbnToolsError('Document not found.', 404, 'not_found'); } $chunks = $db->prepare( 'SELECT id, content, section_title FROM client_chunks WHERE client_id = ? AND document_id = ? ORDER BY id ASC LIMIT 200' ); try { $chunks->execute([$clientId, $id]); $chunkRows = $chunks->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable $e) { $chunkRows = []; } dbnToolsRespond([ 'ok' => true, 'document' => shapeDoc($doc) + ['content' => (string)$doc['content']], 'chunks' => $chunkRows, ]); } function respondUpdate(PDO $db, int $clientId): void { $input = dbnToolsJsonInput(20_000); $id = (int)($input['id'] ?? 0); if ($id <= 0) { dbnToolsError('id is required.', 400, 'missing_id'); } $fields = []; $params = []; $allowed = [ 'title' => ['VARCHAR', 500], 'category' => ['VARCHAR', 50], 'tags' => ['VARCHAR', 500], 'language' => ['VARCHAR', 10], 'author' => ['VARCHAR', 200], ]; foreach ($allowed as $col => [$kind, $max]) { if (!array_key_exists($col, $input)) { continue; } $val = trim((string)$input[$col]); if (mb_strlen($val, 'UTF-8') > $max) { dbnToolsError("Field {$col} exceeds {$max} chars.", 422, 'field_too_long'); } $fields[] = "{$col} = ?"; $params[] = $val !== '' ? $val : null; } if (!$fields) { dbnToolsError('No editable fields supplied.', 400, 'no_fields'); } $params[] = $id; $params[] = $clientId; $stmt = $db->prepare( 'UPDATE client_documents SET ' . implode(', ', $fields) . ', updated_at = NOW() WHERE id = ? AND client_id = ?' ); $stmt->execute($params); $stmt = $db->prepare('SELECT * FROM client_documents WHERE id = ? AND client_id = ? LIMIT 1'); $stmt->execute([$id, $clientId]); $doc = $stmt->fetch(PDO::FETCH_ASSOC); dbnToolsRespond(['ok' => true, 'document' => shapeDoc($doc ?: [])]); } function respondDelete(PDO $db, int $clientId): void { $input = dbnToolsJsonInput(50_000); $ids = $input['ids'] ?? []; if (!is_array($ids) || !$ids) { dbnToolsError('ids array is required.', 400, 'missing_ids'); } $ids = array_values(array_unique(array_map('intval', $ids))); $ids = array_filter($ids, fn($v) => $v > 0); if (!$ids) { dbnToolsError('No valid ids.', 400, 'invalid_ids'); } if (count($ids) > 200) { dbnToolsError('Cannot delete more than 200 documents at once.', 422, 'too_many'); } $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare( "DELETE FROM client_documents WHERE client_id = ? AND id IN ({$placeholders})" ); $stmt->execute(array_merge([$clientId], $ids)); try { $chunks = $db->prepare( "DELETE FROM client_chunks WHERE client_id = ? AND document_id IN ({$placeholders})" ); $chunks->execute(array_merge([$clientId], $ids)); } catch (Throwable $e) { // table may be filtered to client_id only; non-fatal } dbnToolsRespond(['ok' => true, 'deleted' => $stmt->rowCount()]); } function shapeDoc(array $row): array { return [ 'id' => (int)($row['id'] ?? 0), 'title' => (string)($row['title'] ?? ''), 'source_type' => (string)($row['source_type'] ?? ''), 'language' => (string)($row['language'] ?? ''), 'category' => (string)($row['category'] ?? ''), 'tags' => (string)($row['tags'] ?? ''), 'author' => $row['author'] ?? null, 'source_url' => $row['source_url'] ?? null, 'source_tool' => $row['source_tool'] ?? null, 'import_method' => (string)($row['import_method'] ?? ''), 'status' => (string)($row['status'] ?? ''), 'word_count' => (int)($row['word_count'] ?? 0), 'chunk_count' => (int)($row['chunk_count'] ?? 0), 'file_size_bytes'=> (int)($row['file_size_bytes'] ?? 0), 'error_message' => $row['error_message'] ?? null, 'created_at' => (string)($row['created_at'] ?? ''), 'updated_at' => (string)($row['updated_at'] ?? ''), ]; }