/** * Paperless-ngx REST API client. * * Provides typed access to documents, correspondents, tags, and document types. * All methods return immutable result objects. * * @example * ```ts * const client = createPaperlessClient({ * baseUrl: "http://localhost:8000", * token: "your-api-token", * }); * const docs = await client.getDocuments({ query: "invoice" }); * ``` */ import type { Correspondent, DocumentSearchParams, DocumentType, PaginatedResponse, PaperlessConfig, PaperlessDocument, Tag, } from "./types.js"; // --------------------------------------------------------------------------- // Client interface // --------------------------------------------------------------------------- export interface PaperlessClient { /** Fetch a single document by ID. */ getDocument(id: number): Promise; /** Search / list documents with optional filters. */ getDocuments( params?: DocumentSearchParams, ): Promise>; /** Fetch all correspondents. */ getCorrespondents(): Promise>; /** Fetch all tags. */ getTags(): Promise>; /** Fetch all document types. */ getDocumentTypes(): Promise>; /** Download the original file content of a document. */ downloadDocument(id: number): Promise; /** Update tags on a document (immutable -- returns the updated doc). */ updateDocumentTags( id: number, tagIds: readonly number[], ): Promise; } // --------------------------------------------------------------------------- // Implementation // --------------------------------------------------------------------------- /** * Create a new Paperless-ngx API client. * * @param config - Connection configuration (URL + token). * @returns A {@link PaperlessClient} instance. */ export function createPaperlessClient(config: PaperlessConfig): PaperlessClient { const { baseUrl, token, timeout = 30_000 } = config; const headers: Record = { Authorization: `Token ${token}`, "Content-Type": "application/json", Accept: "application/json; version=3", }; /** * Internal fetch wrapper with timeout and error handling. */ async function request( path: string, options: RequestInit = {}, ): Promise { const url = `${baseUrl.replace(/\/+$/, "")}/api${path}`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, headers: { ...headers, ...((options.headers as Record) ?? {}) }, signal: controller.signal, }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error( `Paperless API error: ${response.status} ${response.statusText} -- ${body}`, ); } return (await response.json()) as T; } finally { clearTimeout(timer); } } /** * Build query string from search params. */ function buildQuery(params?: DocumentSearchParams): string { if (!params) return ""; const entries = Object.entries(params).filter( ([, v]) => v !== undefined && v !== null, ); if (entries.length === 0) return ""; const searchParams = new URLSearchParams(); for (const [key, value] of entries) { if (Array.isArray(value)) { searchParams.set(key, value.join(",")); } else { searchParams.set(key, String(value)); } } return `?${searchParams.toString()}`; } return { async getDocument(id) { return request(`/documents/${id}/`); }, async getDocuments(params) { return request>( `/documents/${buildQuery(params)}`, ); }, async getCorrespondents() { return request>("/correspondents/"); }, async getTags() { return request>("/tags/"); }, async getDocumentTypes() { return request>("/document_types/"); }, async downloadDocument(id) { const url = `${baseUrl.replace(/\/+$/, "")}/api/documents/${id}/download/`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { headers: { Authorization: `Token ${token}` }, signal: controller.signal, }); if (!response.ok) { throw new Error( `Paperless download error: ${response.status} ${response.statusText}`, ); } return await response.arrayBuffer(); } finally { clearTimeout(timer); } }, async updateDocumentTags(id, tagIds) { return request(`/documents/${id}/`, { method: "PATCH", body: JSON.stringify({ tags: [...tagIds] }), }); }, }; }