Title, Provider, tags from paperless

This commit is contained in:
MDeeApp
2025-10-11 11:32:04 +02:00
parent 8eb060f380
commit 342a73ecb5
11 changed files with 530 additions and 36 deletions

View File

@@ -365,6 +365,27 @@ app.get("/integrations/paperless/search", async (req, res, next) => {
}
});
app.get("/integrations/paperless/documents/:documentId", async (req, res, next) => {
if (!paperlessClient.isConfigured) {
return res.status(503).json({ error: "Paperless integration not configured" });
}
const documentId = parseId(req.params.documentId);
if (!documentId) {
return res.status(400).json({ error: "Invalid document id" });
}
try {
const document = await paperlessClient.getDocument(documentId);
if (!document) {
return res.status(404).json({ error: "Document not found" });
}
res.json(document);
} catch (error) {
next(error);
}
});
app.get("/contracts", (req, res) => {
const skip = Number(req.query.skip ?? 0);
const limit = Math.min(Number(req.query.limit ?? 100), 500);

View File

@@ -4,6 +4,12 @@ import { getRuntimeSettings } from "./runtimeSettings.js";
const logger = createLogger(config.logLevel);
type PaperlessDocument = Record<string, unknown>;
interface PaperlessCollectionResponse<T> {
results?: T[];
}
export class PaperlessClient {
get isConfigured() {
const { paperlessBaseUrl, paperlessToken } = getRuntimeSettings();
@@ -31,12 +37,23 @@ export class PaperlessClient {
return headers;
}
async getDocument(documentId: number): Promise<Record<string, unknown> | null> {
private async fetchJson<T>(url: URL): Promise<T> {
const response = await fetch(url, { headers: this.getHeaders() });
if (!response.ok) {
const text = await response.text();
logger.error(`Paperless API error ${response.status}: ${text}`);
throw new Error(`Paperless request failed with status ${response.status}`);
}
return response.json() as Promise<T>;
}
async getDocument(documentId: number): Promise<PaperlessDocument | null> {
if (!this.isConfigured) {
throw new Error("Paperless integration is not configured");
}
const url = this.buildUrl(`/api/documents/${documentId}/`);
const url = new URL(this.buildUrl(`/api/documents/${documentId}/`));
url.searchParams.set("metadata", "true");
const response = await fetch(url, { headers: this.getHeaders() });
if (response.status === 404) {
@@ -49,7 +66,9 @@ export class PaperlessClient {
throw new Error(`Paperless request failed with status ${response.status}`);
}
return response.json() as Promise<Record<string, unknown>>;
const document = (await response.json()) as PaperlessDocument;
await this.enrichDocuments([document]);
return document;
}
async searchDocuments(query: string, page = 1): Promise<Record<string, unknown>> {
@@ -60,16 +79,139 @@ export class PaperlessClient {
const url = new URL(this.buildUrl("/api/documents/"));
url.searchParams.set("query", query);
url.searchParams.set("page", page.toString());
url.searchParams.set("metadata", "true");
const response = await fetch(url, { headers: this.getHeaders() });
const payload = await this.fetchJson<Record<string, unknown>>(url);
const results = Array.isArray((payload as PaperlessCollectionResponse<PaperlessDocument>).results)
? ((payload as PaperlessCollectionResponse<PaperlessDocument>).results as PaperlessDocument[])
: [];
await this.enrichDocuments(results);
return payload;
}
if (!response.ok) {
const text = await response.text();
logger.error(`Paperless API error ${response.status}: ${text}`);
throw new Error(`Paperless request failed with status ${response.status}`);
async enrichDocuments(documents: PaperlessDocument[]): Promise<void> {
if (!documents.length) return;
const correspondentIds = new Set<number>();
const tagIds = new Set<number>();
for (const doc of documents) {
if (typeof doc.correspondent === "number") {
correspondentIds.add(doc.correspondent);
}
const tags = Array.isArray(doc.tags) ? doc.tags : [];
for (const tag of tags) {
if (typeof tag === "number") {
tagIds.add(tag);
}
}
}
return response.json() as Promise<Record<string, unknown>>;
const [correspondents, tags] = await Promise.all([
this.fetchCorrespondents(Array.from(correspondentIds)),
this.fetchTags(Array.from(tagIds))
]);
for (const doc of documents) {
if (typeof doc.correspondent === "number" && correspondents.has(doc.correspondent)) {
const correspondent = correspondents.get(doc.correspondent)!;
doc.correspondent_name = correspondent.name ?? correspondent.slug ?? correspondent.title ?? correspondent.value ?? null;
if (!doc.metadata || typeof doc.metadata !== "object") {
doc.metadata = {};
}
(doc.metadata as Record<string, unknown>).correspondent_name = doc.correspondent_name;
}
if (Array.isArray(doc.tags) && doc.tags.length > 0) {
const tagNames: string[] = [];
const tagObjects: Array<Record<string, unknown>> = [];
for (const tag of doc.tags) {
if (typeof tag === "number" && tags.has(tag)) {
const tagData = tags.get(tag)!;
const name =
tagData.name ??
tagData.slug ??
tagData.label ??
tagData.title ??
tagData.value ??
null;
if (name && typeof name === "string") {
tagNames.push(name);
}
tagObjects.push(tagData);
} else if (typeof tag === "string") {
tagNames.push(tag);
} else if (tag && typeof tag === "object") {
tagObjects.push(tag as Record<string, unknown>);
}
}
if (tagNames.length > 0) {
doc.tags__name = tagNames;
if (!doc.metadata || typeof doc.metadata !== "object") {
doc.metadata = {};
}
(doc.metadata as Record<string, unknown>).tag_names = tagNames;
}
if (tagObjects.length > 0) {
doc.tag_details = tagObjects;
}
}
}
}
private async fetchCorrespondents(ids: number[]): Promise<Map<number, Record<string, unknown>>> {
const map = new Map<number, Record<string, unknown>>();
if (ids.length === 0) {
return map;
}
const chunks = this.chunkIds(ids);
for (const chunk of chunks) {
const url = new URL(this.buildUrl("/api/correspondents/"));
url.searchParams.set("id__in", chunk.join(","));
url.searchParams.set("page_size", "100");
const payload = await this.fetchJson<PaperlessCollectionResponse<Record<string, unknown>>>(url);
const results = Array.isArray(payload.results) ? payload.results : [];
for (const item of results) {
if (typeof item.id === "number") {
map.set(item.id, item);
}
}
}
return map;
}
private async fetchTags(ids: number[]): Promise<Map<number, Record<string, unknown>>> {
const map = new Map<number, Record<string, unknown>>();
if (ids.length === 0) {
return map;
}
const chunks = this.chunkIds(ids);
for (const chunk of chunks) {
const url = new URL(this.buildUrl("/api/tags/"));
url.searchParams.set("id__in", chunk.join(","));
url.searchParams.set("page_size", "100");
const payload = await this.fetchJson<PaperlessCollectionResponse<Record<string, unknown>>>(url);
const results = Array.isArray(payload.results) ? payload.results : [];
for (const item of results) {
if (typeof item.id === "number") {
map.set(item.id, item);
}
}
}
return map;
}
private chunkIds(ids: number[], size = 50): number[][] {
const chunks: number[][] = [];
for (let i = 0; i < ids.length; i += size) {
chunks.push(ids.slice(i, i + size));
}
return chunks;
}
}