SurfSense/surfsense_web/lib/apis/documents-api.service.ts

402 lines
12 KiB
TypeScript
Raw Normal View History

2025-11-20 11:00:42 +02:00
import {
type CreateDocumentRequest,
createDocumentRequest,
createDocumentResponse,
type DeleteDocumentRequest,
deleteDocumentRequest,
deleteDocumentResponse,
type GetDocumentByChunkRequest,
type GetDocumentRequest,
type GetDocumentsRequest,
type GetDocumentsStatusRequest,
2025-11-20 11:00:42 +02:00
type GetDocumentTypeCountsRequest,
2026-01-13 01:15:33 +02:00
type GetSurfsenseDocsRequest,
2025-11-20 11:00:42 +02:00
getDocumentByChunkRequest,
getDocumentByChunkResponse,
getDocumentRequest,
getDocumentResponse,
getDocumentsRequest,
2026-02-09 16:49:11 -08:00
getDocumentsResponse,
getDocumentsStatusRequest,
getDocumentsStatusResponse,
2025-11-20 11:00:42 +02:00
getDocumentTypeCountsRequest,
getDocumentTypeCountsResponse,
getSurfsenseDocsByChunkResponse,
getSurfsenseDocsRequest,
2026-01-13 01:15:33 +02:00
getSurfsenseDocsResponse,
2025-11-20 11:00:42 +02:00
type SearchDocumentsRequest,
type SearchDocumentTitlesRequest,
2025-11-20 11:00:42 +02:00
searchDocumentsRequest,
searchDocumentsResponse,
searchDocumentTitlesRequest,
searchDocumentTitlesResponse,
2025-11-20 11:00:42 +02:00
type UpdateDocumentRequest,
type UploadDocumentRequest,
updateDocumentRequest,
updateDocumentResponse,
uploadDocumentRequest,
uploadDocumentResponse,
} from "@/contracts/types/document.types";
import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
class DocumentsApiService {
/**
* Get a list of documents with optional filtering and pagination
*/
getDocuments = async (request: GetDocumentsRequest) => {
const parsedRequest = getDocumentsRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
// Transform query params to be string values
const transformedQueryParams = parsedRequest.data.queryParams
? Object.fromEntries(
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => {
// Handle array values (document_type)
if (Array.isArray(v)) {
return [k, v.join(",")];
}
return [k, String(v)];
})
)
: undefined;
const queryParams = transformedQueryParams
? new URLSearchParams(transformedQueryParams).toString()
: "";
return baseApiService.get(`/api/v1/documents?${queryParams}`, getDocumentsResponse);
};
/**
* Get a single document by ID
*/
getDocument = async (request: GetDocumentRequest) => {
const parsedRequest = getDocumentRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get(`/api/v1/documents/${request.id}`, getDocumentResponse);
};
/**
* Create documents (extension, crawled URL, or YouTube video)
*/
createDocument = async (request: CreateDocumentRequest) => {
const parsedRequest = createDocumentRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.post(`/api/v1/documents`, createDocumentResponse, {
body: parsedRequest.data,
});
};
/**
* Upload document files in batches to avoid proxy/LB timeouts.
* Files are split into chunks of UPLOAD_BATCH_SIZE and sent as separate
* requests. Results are aggregated into a single response.
2025-11-20 11:00:42 +02:00
*/
uploadDocument = async (request: UploadDocumentRequest) => {
const parsedRequest = uploadDocumentRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { files, search_space_id, should_summarize } = parsedRequest.data;
const UPLOAD_BATCH_SIZE = 5;
2025-11-20 11:00:42 +02:00
const batches: File[][] = [];
for (let i = 0; i < files.length; i += UPLOAD_BATCH_SIZE) {
batches.push(files.slice(i, i + UPLOAD_BATCH_SIZE));
}
const allDocumentIds: number[] = [];
const allDuplicateIds: number[] = [];
let totalFiles = 0;
let pendingFiles = 0;
let skippedDuplicates = 0;
for (const batch of batches) {
const formData = new FormData();
2026-03-27 03:17:05 -07:00
for (const file of batch) formData.append("files", file);
formData.append("search_space_id", String(search_space_id));
formData.append("should_summarize", String(should_summarize));
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120_000);
try {
const result = await baseApiService.postFormData(
`/api/v1/documents/fileupload`,
uploadDocumentResponse,
{ body: formData, signal: controller.signal }
);
allDocumentIds.push(...(result.document_ids ?? []));
allDuplicateIds.push(...(result.duplicate_document_ids ?? []));
totalFiles += result.total_files ?? batch.length;
pendingFiles += result.pending_files ?? 0;
skippedDuplicates += result.skipped_duplicates ?? 0;
} finally {
clearTimeout(timeoutId);
}
}
return {
message: "Files uploaded for processing" as const,
document_ids: allDocumentIds,
duplicate_document_ids: allDuplicateIds,
total_files: totalFiles,
pending_files: pendingFiles,
skipped_duplicates: skippedDuplicates,
};
2025-11-20 11:00:42 +02:00
};
/**
* Batch document status for async processing tracking
*/
getDocumentsStatus = async (request: GetDocumentsStatusRequest) => {
const parsedRequest = getDocumentsStatusRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { search_space_id, document_ids } = parsedRequest.data.queryParams;
const params = new URLSearchParams({
search_space_id: String(search_space_id),
document_ids: document_ids.join(","),
});
2026-02-09 16:49:11 -08:00
return baseApiService.get(
`/api/v1/documents/status?${params.toString()}`,
getDocumentsStatusResponse
);
};
2025-11-20 11:00:42 +02:00
/**
* Search documents by title
*/
searchDocuments = async (request: SearchDocumentsRequest) => {
const parsedRequest = searchDocumentsRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
// Transform query params to be string values
const transformedQueryParams = parsedRequest.data.queryParams
? Object.fromEntries(
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => {
// Handle array values (document_type)
if (Array.isArray(v)) {
return [k, v.join(",")];
}
return [k, String(v)];
})
)
: undefined;
const queryParams = transformedQueryParams
? new URLSearchParams(transformedQueryParams).toString()
: "";
return baseApiService.get(`/api/v1/documents/search?${queryParams}`, searchDocumentsResponse);
};
/**
* Search document titles (lightweight, optimized for mention picker)
* Returns only id, title, document_type - no content or metadata
* @param request - The search request with query params
* @param signal - Optional AbortSignal for request cancellation
*/
searchDocumentTitles = async (request: SearchDocumentTitlesRequest, signal?: AbortSignal) => {
const parsedRequest = searchDocumentTitlesRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
// Transform query params to be string values
const transformedQueryParams = Object.fromEntries(
Object.entries(parsedRequest.data.queryParams)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
);
const queryParams = new URLSearchParams(transformedQueryParams).toString();
return baseApiService.get(
`/api/v1/documents/search/titles?${queryParams}`,
searchDocumentTitlesResponse,
{ signal }
);
};
2025-11-20 11:00:42 +02:00
/**
* Get document type counts
*/
getDocumentTypeCounts = async (request: GetDocumentTypeCountsRequest) => {
// Validate the request
const parsedRequest = getDocumentTypeCountsRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
// Format a user friendly error message
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
// Transform query params to be string values
const transformedQueryParams = parsedRequest.data.queryParams
? Object.fromEntries(
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
)
: undefined;
const queryParams = transformedQueryParams
? new URLSearchParams(transformedQueryParams).toString()
: "";
return baseApiService.get(
`/api/v1/documents/type-counts?${queryParams}`,
getDocumentTypeCountsResponse
);
};
/**
* Get document by chunk ID (includes all chunks)
*/
getDocumentByChunk = async (request: GetDocumentByChunkRequest) => {
// Validate the request
const parsedRequest = getDocumentByChunkRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
// Format a user friendly error message
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.get(
`/api/v1/documents/by-chunk/${request.chunk_id}`,
getDocumentByChunkResponse
);
};
/**
* Get Surfsense documentation by chunk ID
* Used for resolving [citation:doc-XXX] citations
*/
getSurfsenseDocByChunk = async (chunkId: number) => {
return baseApiService.get(
`/api/v1/surfsense-docs/by-chunk/${chunkId}`,
getSurfsenseDocsByChunkResponse
);
};
2026-01-13 01:15:33 +02:00
/**
* List all Surfsense documentation documents
* @param request - The request with query params
* @param signal - Optional AbortSignal for request cancellation
2026-01-13 01:15:33 +02:00
*/
getSurfsenseDocs = async (request: GetSurfsenseDocsRequest, signal?: AbortSignal) => {
const parsedRequest = getSurfsenseDocsRequest.safeParse(request);
2026-01-13 01:15:33 +02:00
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
2026-01-13 01:15:33 +02:00
}
// Transform query params to be string values
const transformedQueryParams = parsedRequest.data.queryParams
? Object.fromEntries(
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
)
: undefined;
const queryParams = transformedQueryParams
? new URLSearchParams(transformedQueryParams).toString()
: "";
const url = `/api/v1/surfsense-docs?${queryParams}`;
2026-01-13 01:15:33 +02:00
return baseApiService.get(url, getSurfsenseDocsResponse, { signal });
2026-01-13 01:15:33 +02:00
};
2025-11-20 11:00:42 +02:00
/**
* Update a document
*/
updateDocument = async (request: UpdateDocumentRequest) => {
// Validate the request
const parsedRequest = updateDocumentRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
// Format a user friendly error message
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { id, data } = parsedRequest.data;
return baseApiService.put(`/api/v1/documents/${id}`, updateDocumentResponse, {
body: data,
});
};
/**
* Delete a document
*/
deleteDocument = async (request: DeleteDocumentRequest) => {
// Validate the request
const parsedRequest = deleteDocumentRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
// Format a user friendly error message
2025-12-20 05:10:47 -08:00
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
2025-11-20 11:00:42 +02:00
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
return baseApiService.delete(`/api/v1/documents/${request.id}`, deleteDocumentResponse);
};
}
export const documentsApiService = new DocumentsApiService();