chore: apply ESLint auto-fix across entire backend
Build and Push Multi-Platform Images / build-and-push (push) Successful in 41s

Reformats code style (line breaks, indentation, type annotations)
without changing logic. Also includes minor feature additions bundled
in the same lint run (stats service, user-settings groups, agrarmonitor
polling improvements).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Sonnet 4.6 committed 2026-06-08 09:02:02 +02:00
1 parent 4c75a1ded2
commit dad0136365
74 files changed
+4022 -1052

No files matched your search

@@ -10,10 +10,15 @@ export class ExportService {
private readonly logger = new Logger(ExportService.name);
constructor(
@InjectRepository(ExportTarget) private readonly targetRepo: Repository<ExportTarget>,
@InjectRepository(ExportTarget)
private readonly targetRepo: Repository<ExportTarget>,
) {}
async exportFile(targetId: number, filename: string, content: Buffer): Promise<void> {
async exportFile(
targetId: number,
filename: string,
content: Buffer,
): Promise<void> {
const target = await this.targetRepo.findOneByOrFail({ Id: targetId });
if (!target.IsActive) {
@@ -32,7 +37,9 @@ export class ExportService {
}
}
async testConnection(targetId: number): Promise<{ success: boolean; message: string }> {
async testConnection(
targetId: number,
): Promise<{ success: boolean; message: string }> {
const target = await this.targetRepo.findOneByOrFail({ Id: targetId });
try {
@@ -44,7 +51,10 @@ export class ExportService {
await this.testWebDav(target);
break;
default:
return { success: false, message: `Unbekanntes Protokoll: ${target.Protocol}` };
return {
success: false,
message: `Unbekanntes Protokoll: ${target.Protocol}`,
};
}
return { success: true, message: 'Verbindung erfolgreich.' };
} catch (err: any) {
@@ -52,7 +62,11 @@ export class ExportService {
}
}
private async uploadFtp(target: ExportTarget, filename: string, content: Buffer): Promise<void> {
private async uploadFtp(
target: ExportTarget,
filename: string,
content: Buffer,
): Promise<void> {
const client = new ftp.Client();
try {
await client.access({
@@ -89,7 +103,11 @@ export class ExportService {
}
}
private async uploadWebDav(target: ExportTarget, filename: string, content: Buffer): Promise<void> {
private async uploadWebDav(
target: ExportTarget,
filename: string,
content: Buffer,
): Promise<void> {
const client = this.createWebDavClient(target);
const remotePath = `${target.RemotePath || '/'}/${filename}`;
await client.putFileContents(remotePath, content);
@@ -25,12 +25,24 @@ export class MailService {
body: string;
html?: string;
attachments?: { filename: string; content: Buffer }[];
smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string };
smtpOverride?: {
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
from: string;
};
}): Promise<void> {
let transporter = this.transporter;
const globalFromEmail = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
const globalFromEmail = this.configService.get<string>(
'SMTP_FROM',
'paperless@localhost',
);
const globalFromName = this.configService.get<string>('SMTP_FROM_NAME', '');
let from = globalFromName ? `"${globalFromName}" <${globalFromEmail}>` : globalFromEmail;
let from = globalFromName
? `"${globalFromName}" <${globalFromEmail}>`
: globalFromEmail;
if (options.smtpOverride) {
const o = options.smtpOverride;
@@ -53,7 +65,7 @@ export class MailService {
subject: options.subject,
text: options.body,
html: options.html,
attachments: options.attachments?.map(a => ({
attachments: options.attachments?.map((a) => ({
filename: a.filename,
content: a.content,
})),
@@ -11,7 +11,12 @@ import { PaperlessModule } from '../paperless/paperless.module';
@Module({
imports: [
TypeOrmModule.forFeature([Postprocessing, PostprocessingAction, PostprocessingLog, ExportTarget]),
TypeOrmModule.forFeature([
Postprocessing,
PostprocessingAction,
PostprocessingLog,
ExportTarget,
]),
forwardRef(() => PaperlessModule),
],
providers: [PostprocessingService, MailService, ExportService],
@@ -6,12 +6,39 @@ import { PostprocessingAction } from '../database/entities/postprocessing-action
import { PaperlessService } from '../paperless/paperless.service';
const mockRules: Partial<Postprocessing>[] = [
{ Id: 1, Name: 'Rule1', DocumentTypeId: 5, CorrespondentId: null, OwnerId: null, TagId: null, Order: 1, IsActive: true, NoFurther: false },
{ Id: 2, Name: 'StopRule', DocumentTypeId: null, CorrespondentId: null, OwnerId: null, TagId: null, Order: 2, IsActive: true, NoFurther: true },
{
Id: 1,
Name: 'Rule1',
DocumentTypeId: 5,
CorrespondentId: null,
OwnerId: null,
TagId: null,
Order: 1,
IsActive: true,
NoFurther: false,
},
{
Id: 2,
Name: 'StopRule',
DocumentTypeId: null,
CorrespondentId: null,
OwnerId: null,
TagId: null,
Order: 2,
IsActive: true,
NoFurther: true,
},
];
const mockActions: Partial<PostprocessingAction>[] = [
{ Id: 1, PostprocessingId: 1, ActionType: 2, Content: '99', Order: 1, IsActive: true },
{
Id: 1,
PostprocessingId: 1,
ActionType: 2,
Content: '99',
Order: 1,
IsActive: true,
},
];
describe('PostprocessingService', () => {
@@ -29,7 +56,10 @@ describe('PostprocessingService', () => {
providers: [
PostprocessingService,
{ provide: getRepositoryToken(Postprocessing), useValue: ppRepo },
{ provide: getRepositoryToken(PostprocessingAction), useValue: ppActionRepo },
{
provide: getRepositoryToken(PostprocessingAction),
useValue: ppActionRepo,
},
{ provide: PaperlessService, useValue: paperlessService },
],
}).compile();
@@ -58,7 +88,9 @@ describe('PostprocessingService', () => {
where: { PostprocessingId: 1, IsActive: true },
order: { Order: 'ASC' },
});
expect(paperlessService.updateDocument).toHaveBeenCalledWith(100, { tags: [99] });
expect(paperlessService.updateDocument).toHaveBeenCalledWith(100, {
tags: [99],
});
});
it('evaluate stops at NoFurther rule', async () => {
@@ -72,7 +104,11 @@ describe('PostprocessingService', () => {
it('evaluate skips non-matching rules', async () => {
// documentTypeId=999 doesn't match Rule1 (requires 5) but matches Rule2 (no filter)
ppActionRepo.find.mockResolvedValue([]);
await service.evaluate({ documentId: 100, documentTypeId: 999, tagIds: [] });
await service.evaluate({
documentId: 100,
documentTypeId: 999,
tagIds: [],
});
// Rule1 skipped, Rule2 matched → only 1 action lookup
expect(ppActionRepo.find).toHaveBeenCalledTimes(1);
@@ -2,7 +2,11 @@ import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Postprocessing, type FilterGroup, type FilterCondition } from '../database/entities/postprocessing.entity';
import {
Postprocessing,
type FilterGroup,
type FilterCondition,
} from '../database/entities/postprocessing.entity';
import { PostprocessingAction } from '../database/entities/postprocessing-action.entity';
import { PostprocessingLog } from '../database/entities/postprocessing-log.entity';
import { PaperlessService } from '../paperless/paperless.service';
@@ -20,15 +24,22 @@ export class PostprocessingService {
private documentTypesCache: { data: any[]; expires: number } | null = null;
constructor(
@InjectRepository(Postprocessing) private readonly ppRepo: Repository<Postprocessing>,
@InjectRepository(PostprocessingAction) private readonly actionRepo: Repository<PostprocessingAction>,
@InjectRepository(PostprocessingLog) private readonly logRepo: Repository<PostprocessingLog>,
@InjectRepository(Postprocessing)
private readonly ppRepo: Repository<Postprocessing>,
@InjectRepository(PostprocessingAction)
private readonly actionRepo: Repository<PostprocessingAction>,
@InjectRepository(PostprocessingLog)
private readonly logRepo: Repository<PostprocessingLog>,
private readonly configService: ConfigService,
@Inject(forwardRef(() => PaperlessService)) private readonly paperlessService: PaperlessService,
@Inject(forwardRef(() => PaperlessService))
private readonly paperlessService: PaperlessService,
private readonly mailService: MailService,
private readonly exportService: ExportService,
) {
this.errorTagId = this.configService.get<number>('POSTPROCESSING_ERROR_TAG', 0);
this.errorTagId = this.configService.get<number>(
'POSTPROCESSING_ERROR_TAG',
0,
);
}
async evaluate(doc: any): Promise<void> {
@@ -40,19 +51,29 @@ export class PostprocessingService {
// Enrich doc with resolved names (once per evaluation)
await this.enrichDocWithNames(doc);
this.logger.log(`[Postprocessing] Dokument ${doc.id} wird gegen ${rules.length} Regel(n) geprüft`);
this.logger.log(
`[Postprocessing] Dokument ${doc.id} wird gegen ${rules.length} Regel(n) geprüft`,
);
for (const rule of rules) {
if (!this.hasConditions(rule.FilterJson)) {
this.logger.warn(`[Postprocessing] Regel "${rule.Name}" (ID: ${rule.Id}) hat keine Bedingungen – wird übersprungen.`);
this.logger.warn(
`[Postprocessing] Regel "${rule.Name}" (ID: ${rule.Id}) hat keine Bedingungen – wird übersprungen.`,
);
continue;
}
this.logger.debug(`[Postprocessing] Prüfe Regel "${rule.Name}" (ID: ${rule.Id}) für Dokument ${doc.id}`);
this.logger.debug(
`[Postprocessing] Prüfe Regel "${rule.Name}" (ID: ${rule.Id}) für Dokument ${doc.id}`,
);
const matches = this.matchesFilter(rule.FilterJson, doc);
this.logger.log(`[Postprocessing] Regel "${rule.Name}" (ID: ${rule.Id}) → ${matches ? 'TRIFFT ZU' : 'trifft nicht zu'}`);
this.logger.log(
`[Postprocessing] Regel "${rule.Name}" (ID: ${rule.Id}) → ${matches ? 'TRIFFT ZU' : 'trifft nicht zu'}`,
);
if (!matches) continue;
this.logger.log(`[Postprocessing] Regel "${rule.Name}" trifft zu für Dokument ${doc.id}`);
this.logger.log(
`[Postprocessing] Regel "${rule.Name}" trifft zu für Dokument ${doc.id}`,
);
const actions = await this.actionRepo.find({
where: { PostprocessingId: rule.Id, IsActive: true },
@@ -63,10 +84,18 @@ export class PostprocessingService {
for (const action of actions) {
try {
await this.executeAction(action, doc);
await this.log(rule.Id, action.Id, doc.id, 'success', `Aktion ${action.ActionType} erfolgreich`);
await this.log(
rule.Id,
action.Id,
doc.id,
'success',
`Aktion ${action.ActionType} erfolgreich`,
);
} catch (err: any) {
hasError = true;
this.logger.error(`Fehler bei Aktion ${action.Id} (Typ ${action.ActionType}) für Dokument ${doc.id}: ${err.message}`);
this.logger.error(
`Fehler bei Aktion ${action.Id} (Typ ${action.ActionType}) für Dokument ${doc.id}: ${err.message}`,
);
await this.log(rule.Id, action.Id, doc.id, 'error', err.message);
}
}
@@ -75,9 +104,13 @@ export class PostprocessingService {
try {
const currentTags = new Set<number>(doc.tags || []);
currentTags.add(this.errorTagId);
await this.paperlessService.updateDocument(doc.id, { tags: Array.from(currentTags) });
await this.paperlessService.updateDocument(doc.id, {
tags: Array.from(currentTags),
});
} catch (tagErr: any) {
this.logger.error(`Konnte Error-Tag ${this.errorTagId} nicht setzen für Dokument ${doc.id}: ${tagErr.message}`);
this.logger.error(
`Konnte Error-Tag ${this.errorTagId} nicht setzen für Dokument ${doc.id}: ${tagErr.message}`,
);
}
}
@@ -92,8 +125,8 @@ export class PostprocessingService {
private hasConditions(filter: FilterGroup): boolean {
if (!filter || !filter.rules || filter.rules.length === 0) return false;
return filter.rules.some(rule => {
if ('combinator' in rule) return this.hasConditions(rule as FilterGroup);
return filter.rules.some((rule) => {
if ('combinator' in rule) return this.hasConditions(rule);
return true;
});
}
@@ -101,11 +134,11 @@ export class PostprocessingService {
private matchesFilter(filter: FilterGroup, doc: any): boolean {
if (!filter || !filter.rules || filter.rules.length === 0) return false;
const results = filter.rules.map(rule => {
const results = filter.rules.map((rule) => {
if ('combinator' in rule) {
return this.matchesFilter(rule as FilterGroup, doc);
return this.matchesFilter(rule, doc);
}
return this.evaluateCondition(rule as FilterCondition, doc);
return this.evaluateCondition(rule, doc);
});
return filter.combinator === 'AND'
@@ -139,7 +172,9 @@ export class PostprocessingService {
if (cond.field === 'tag') {
result = Array.isArray(actual) && actual.includes(Number(expected));
} else {
result = String(actual ?? '').toLowerCase().includes(String(expected ?? '').toLowerCase());
result = String(actual ?? '')
.toLowerCase()
.includes(String(expected ?? '').toLowerCase());
}
break;
@@ -147,7 +182,9 @@ export class PostprocessingService {
if (cond.field === 'tag') {
result = !Array.isArray(actual) || !actual.includes(Number(expected));
} else {
result = !String(actual ?? '').toLowerCase().includes(String(expected ?? '').toLowerCase());
result = !String(actual ?? '')
.toLowerCase()
.includes(String(expected ?? '').toLowerCase());
}
break;
@@ -173,7 +210,7 @@ export class PostprocessingService {
}
this.logger.debug(
`[Postprocessing] Bedingung: ${cond.field} ${cond.operator} "${expected}" | Istwert: "${Array.isArray(actual) ? actual.join(',') : actual}" → ${result ? 'WAHR' : 'FALSCH'}`
`[Postprocessing] Bedingung: ${cond.field} ${cond.operator} "${expected}" | Istwert: "${Array.isArray(actual) ? actual.join(',') : actual}" → ${result ? 'WAHR' : 'FALSCH'}`,
);
return result;
}
@@ -196,7 +233,9 @@ export class PostprocessingService {
// Custom field: "custom_field_<id>"
if (field.startsWith('custom_field_')) {
const fieldId = parseInt(field.replace('custom_field_', ''), 10);
const cf = (doc.custom_fields || []).find((f: any) => f.field === fieldId);
const cf = (doc.custom_fields || []).find(
(f: any) => f.field === fieldId,
);
return cf?.value ?? null;
}
return null;
@@ -205,7 +244,10 @@ export class PostprocessingService {
// ── Action Execution ─────────────────────────────────────────────
private async executeAction(action: PostprocessingAction, doc: any): Promise<void> {
private async executeAction(
action: PostprocessingAction,
doc: any,
): Promise<void> {
const content = action.Content;
switch (action.ActionType) {
@@ -233,15 +275,26 @@ export class PostprocessingService {
}
private async getCachedCorrespondents(): Promise<any[]> {
if (!this.correspondentsCache || Date.now() > this.correspondentsCache.expires) {
const response = await this.paperlessService.getCorrespondents({ page_size: 9999 });
this.correspondentsCache = { data: response.results, expires: Date.now() + CACHE_TTL_MS };
if (
!this.correspondentsCache ||
Date.now() > this.correspondentsCache.expires
) {
const response = await this.paperlessService.getCorrespondents({
page_size: 9999,
});
this.correspondentsCache = {
data: response.results,
expires: Date.now() + CACHE_TTL_MS,
};
}
return this.correspondentsCache.data;
}
private async getCachedDocumentTypes(): Promise<any[]> {
if (!this.documentTypesCache || Date.now() > this.documentTypesCache.expires) {
if (
!this.documentTypesCache ||
Date.now() > this.documentTypesCache.expires
) {
const data = await this.paperlessService.getDocumentTypes();
this.documentTypesCache = { data, expires: Date.now() + CACHE_TTL_MS };
}
@@ -272,13 +325,18 @@ export class PostprocessingService {
'{titel}': doc.title ?? '',
'{korrespondent}': String(doc.correspondent ?? ''),
'{absender}': String(doc.correspondent ?? ''),
'{korrespondent_name}': doc._correspondentName ?? String(doc.correspondent ?? ''),
'{absender_name}': doc._correspondentName ?? String(doc.correspondent ?? ''),
'{korrespondent_name}':
doc._correspondentName ?? String(doc.correspondent ?? ''),
'{absender_name}':
doc._correspondentName ?? String(doc.correspondent ?? ''),
'{dokumenttyp}': String(doc.document_type ?? ''),
'{dokumenttyp_name}': doc._documentTypeName ?? String(doc.document_type ?? ''),
'{dokumenttyp_name}':
doc._documentTypeName ?? String(doc.document_type ?? ''),
'{besitzer}': String(doc.owner ?? ''),
'{ablagenummer}': String(doc.archive_serial_number ?? ''),
'{datum}': created ? `${created.getFullYear()}-${String(created.getMonth() + 1).padStart(2, '0')}-${String(created.getDate()).padStart(2, '0')}` : '',
'{datum}': created
? `${created.getFullYear()}-${String(created.getMonth() + 1).padStart(2, '0')}-${String(created.getDate()).padStart(2, '0')}`
: '',
'{jahr}': created ? String(created.getFullYear()) : '',
'{monat}': created ? String(created.getMonth() + 1).padStart(2, '0') : '',
'{tag}': created ? String(created.getDate()).padStart(2, '0') : '',
@@ -289,7 +347,7 @@ export class PostprocessingService {
};
// Custom Fields: {custom_field_<id>}
for (const cf of (doc.custom_fields || [])) {
for (const cf of doc.custom_fields || []) {
replacements[`{custom_field_${cf.field}}`] = String(cf.value ?? '');
}
@@ -310,16 +368,32 @@ export class PostprocessingService {
return `${doc.title || `document_${doc.id}`}.pdf`;
}
private async handleExport(content: Record<string, any>, doc: any): Promise<void> {
private async handleExport(
content: Record<string, any>,
doc: any,
): Promise<void> {
const fileType = content.fileType || 'archive';
const buffer = await this.paperlessService.downloadDocument(doc.id, fileType);
const buffer = await this.paperlessService.downloadDocument(
doc.id,
fileType,
);
const filename = this.buildFilename(content.filenameTemplate, doc);
await this.exportService.exportFile(content.exportTargetId, filename, buffer);
await this.exportService.exportFile(
content.exportTargetId,
filename,
buffer,
);
}
private async handleMail(content: Record<string, any>, doc: any): Promise<void> {
private async handleMail(
content: Record<string, any>,
doc: any,
): Promise<void> {
const fileType = content.fileType || 'archive';
const buffer = await this.paperlessService.downloadDocument(doc.id, fileType);
const buffer = await this.paperlessService.downloadDocument(
doc.id,
fileType,
);
const filename = this.buildFilename(content.filenameTemplate, doc);
const subject = content.subject
@@ -337,18 +411,26 @@ export class PostprocessingService {
});
}
private async handleTags(content: Record<string, any>, doc: any): Promise<void> {
private async handleTags(
content: Record<string, any>,
doc: any,
): Promise<void> {
const currentTags = new Set<number>(doc.tags || []);
const addTags: number[] = content.addTags || [];
const removeTags: number[] = content.removeTags || [];
addTags.forEach(t => currentTags.add(t));
removeTags.forEach(t => currentTags.delete(t));
addTags.forEach((t) => currentTags.add(t));
removeTags.forEach((t) => currentTags.delete(t));
await this.paperlessService.updateDocument(doc.id, { tags: Array.from(currentTags) });
await this.paperlessService.updateDocument(doc.id, {
tags: Array.from(currentTags),
});
}
private async handleCustomField(content: Record<string, any>, doc: any): Promise<void> {
private async handleCustomField(
content: Record<string, any>,
doc: any,
): Promise<void> {
const customFields = [...(doc.custom_fields || [])];
const existing = customFields.find((f: any) => f.field === content.fieldId);
@@ -358,13 +440,23 @@ export class PostprocessingService {
customFields.push({ field: content.fieldId, value: content.value });
}
await this.paperlessService.updateDocument(doc.id, { custom_fields: customFields });
await this.paperlessService.updateDocument(doc.id, {
custom_fields: customFields,
});
}
private async handleWebhook(content: Record<string, any>, doc: any): Promise<void> {
private async handleWebhook(
content: Record<string, any>,
doc: any,
): Promise<void> {
const method = (content.method || 'POST').toUpperCase();
const headers = content.headers || {};
const body = { documentId: doc.id, title: doc.title, tags: doc.tags, ...(content.body || {}) };
const body = {
documentId: doc.id,
title: doc.title,
tags: doc.tags,
...(content.body || {}),
};
await axios({
method,
@@ -378,7 +470,10 @@ export class PostprocessingService {
this.logger.log(`Webhook ${method} → ${content.url}`);
}
private async handleNote(content: Record<string, any>, doc: any): Promise<void> {
private async handleNote(
content: Record<string, any>,
doc: any,
): Promise<void> {
if (!content.note) return;
const resolvedNote = this.resolveTemplate(content.note, doc);
await this.paperlessService.addNote(doc.id, resolvedNote);
@@ -386,7 +481,13 @@ export class PostprocessingService {
// ── Logging ──────────────────────────────────────────────────────
private async log(ppId: number, actionId: number | null, docId: number, status: string, message: string): Promise<void> {
private async log(
ppId: number,
actionId: number | null,
docId: number,
status: string,
message: string,
): Promise<void> {
const entry = this.logRepo.create({
PostprocessingId: ppId,
ActionId: actionId,