# tsreport-sdk [English](./README.md) | [日本語](./README.ja.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [Tiếng Việt](./README.vi.md) | [ไทย](./README.th.md) | [Bahasa Indonesia](./README.id.md) | Deutsch | [Français](./README.fr.md) | [Español](./README.es.md) | [Português](./README.pt.md) | [العربية](./README.ar.md) | [עברית](./README.he.md) **Ein Client, um einen Berichtsserver mit dem Druck von PDFs zu beauftragen und die Ergebnisse entgegenzunehmen. Keine Laufzeit-Abhängigkeiten. Für die Nutzung auf der Serverseite (Node.js) konzipiert.** tsreport-sdk übernimmt die gesamte Kommunikation mit dem externen Druck-API-Server, den tsreport-editor bereitstellt. Authentifizierung über OAuth 2.0, das Einreichen von Druckaufträgen, das Warten auf deren Abschluss, das Herunterladen von PDFs und sogar das Abrufen von Material für die Browser-Vorschau lassen sich als eine einzige, typisierte Klasse behandeln. Das Abrufen von Tokens, die Verwaltung ihrer Gültigkeitsdauer sowie die erneute Beschaffung bei Ablauf werden vollständig innerhalb der Bibliothek erledigt, sodass die Nutzerseite lediglich festlegen muss, „welche Vorlage mit welchen Daten befüllt werden soll". ## Architektur: Es gibt zwei Authentifizierungsgrenzen Ein System, das dieses SDK einbindet, besteht aus drei Schichten. **Verschaffen Sie sich zuerst einen Überblick über das Gesamtbild.** Die meisten Missverständnisse rund um dieses SDK entstehen dadurch, dass die beiden Authentifizierungsgrenzen miteinander verwechselt werden. ```mermaid flowchart LR browser["Browser\ncreateEndpointConnector\n(besitzt keine Zugangsdaten)"] app["Eigener Anwendungsserver\ncreatePreviewEndpoint + TsreportClient\n(clientSecret nur hier)"] editor["Druck-API-Server\n(tsreport-editor)"] browser -->|"Grenze A: Sitzungsauthentifizierung\n(selbst zu implementieren)"| app app -->|"Grenze B: OAuth 2.0\n(vom SDK automatisch verarbeitet)"| editor ``` | Schicht | Verwendete SDK-Komponente | Rolle bei der Authentifizierung | | --- | --- | --- | | Browser | `createEndpointConnector()` | **Empfängerseite** der Authentifizierung an Grenze A. Sendet lediglich z. B. Sitzungs-Cookies und besitzt keinerlei Zugangsdaten | | Eigener Anwendungsserver | `createPreviewEndpoint()`+`TsreportClient` | **Prüft** Grenze A (diese Prüfung ist eine eigene Implementierung der Anwendung). Grenze B wird dem SDK überlassen. `clientSecret` liegt ausschließlich in dieser Schicht | | Druck-API-Server | (bereitgestellt von tsreport-editor) | Prüft Grenze B | **Das SDK kümmert sich nur um Grenze B.** Das Abrufen, die Gültigkeitsverwaltung und das erneute Senden von Tokens mittels `clientId`/`clientSecret` werden vollständig intern von `TsreportClient` übernommen. Andererseits **existiert an keiner Stelle dieses SDKs ein Mechanismus, der Grenze A bewertet — nämlich „wer gerade handelt und ob diese Person diesen Bericht einsehen darf".** `createPreviewEndpoint()` ist eine reine Durchleitung ohne jegliche Autorisierungsprüfung. Anmeldung, Sitzungsverwaltung und Berechtigungsprüfung müssen von der Anwendung selbst bereitgestellt werden und **müssen zwingend vor dem Endpunkt platziert werden**. Wird dies ausgelassen, kann jeder, der die URL erreicht, Berichte und Material einsehen. Die „Sitzungsprüfung" und `requireAppUser`, die in den folgenden Beispielen auftauchen, sind keine Verzierung. Es handelt sich um **eigenen Code für Grenze A, dessen Implementierung auf Anwendungsseite zwingend erforderlich ist**. ## Was dieses Paket tut und was nicht Dieses Paket ist ausschließlich für die **Kommunikation** zuständig. - **Was es tut** — Authentifizierung (OAuth 2.0 Client Credentials), Aufruf der Druck-API, Polling des Auftragsstatus, Abruf von PDFs, Abruf von Vorschaumaterial und Bereitstellung des Durchleitungs-Endpunkts, der auf dem Anwendungsserver platziert wird - **Wo es läuft** — `TsreportClient` und `tsreport-sdk/server` sind ausschließlich für die Serverseite. Im Browser nutzbar ist nur `createEndpointConnector()`, das keinerlei Zugangsdaten besitzt - **Was es nicht tut** — Layout von Berichten oder PDF-Erzeugung (Aufgabe von `tsreport-core`), das Rendering der Vorschau (Aufgabe von `tsreport-react`) sowie **Authentifizierung und Autorisierung des Nutzers** (Grenze A, Aufgabe der Anwendung auf Nutzerseite) Es gibt außerdem zwei konzeptionelle Zusicherungen. **Keine Laufzeit-Abhängigkeiten** (in `package.json` gibt es keine `dependencies`), und **es werden keinerlei Umgebungsvariablen gelesen**. Sowohl das Ziel der Verbindung als auch die Zugangsdaten werden vollständig als explizite Argumente entgegengenommen, sodass sich das Verhalten unabhängig von der Umgebung nicht ändert. ## Installation ```sh npm install tsreport-sdk ``` Funktioniert unter Node.js 18 oder höher. Intern werden ausschließlich `fetch` und Web Streams verwendet; es besteht keine Abhängigkeit von Node.js-spezifischen APIs. **Bitte verwenden Sie diese Bibliothek auf der Serverseite.** Der zentrale `TsreportClient` benötigt ein `clientSecret`; wird er im Browser ausgeführt, wird der geheime Schlüssel an alle Nutzer verteilt. Wenn Sie Berichte im Browser handhaben möchten, verwenden Sie die im Abschnitt „Vorschau im Browser" beschriebene dreischichtige Architektur, wobei auf der Browserseite ausschließlich `createEndpointConnector()` ohne Zugangsdaten verwendet wird. ## Voraussetzungen Diese Bibliothek funktioniert nicht eigenständig. **Voraussetzung ist, dass der externe Druck-API-Server von tsreport-editor läuft.** Beschaffen Sie sich von diesem Server die folgenden vier Angaben. | Erforderlich | Beschreibung | | --- | --- | | Basis-URL | Die URL des API-Servers (Beispiel: `https://reports.example.com`). Ein abschließender Schrägstrich ist unproblematisch | | Client-ID | Die client_id von OAuth 2.0 | | Client-Secret | Das client_secret von OAuth 2.0. **Darf ausschließlich serverseitig gehalten und keinesfalls an den Browser weitergegeben werden** | | Workspace-Key | Der Bezeichner (im UUID-Format) des Workspace, in dem die Berichte abgelegt sind | Dem Client werden je nach Verwendungszweck Scopes zugewiesen. Es gibt vier Arten: `report:print` (Einreichen des Drucks), `report:status` (Statusabfrage), `report:download` (Abruf des PDF) und `report:preview` (Abruf von Vorschaumaterial). ## Der erste Schritt: Einen Bericht als PDF entgegennehmen Der kürzeste Code, um von der Übergabe von Vorlage und Daten bis zum Erhalt der PDF-Bytes zu gelangen. ```ts import { writeFileSync } from 'node:fs' import { TsreportClient } from 'tsreport-sdk' const client = new TsreportClient({ baseUrl: 'https://reports.example.com', clientId: 'my-client-id', clientSecret: 'my-client-secret', }) const pdf = await client.printAndDownload( '00000000-0000-0000-0000-000000000002', // Workspace-Schlüssel 'invoice.report', // Pfad des Templates im Workspace 'v1', // Tag (Version) des Templates { rows: [{ item: 'Bauteil A', amount: 12000 }] }, // Daten, die in den Bericht eingespeist werden ) writeFileSync('./invoice.pdf', pdf) ``` `printAndDownload()` führt die drei nachfolgend erläuterten Schritte „Einreichen → Auf Abschluss warten → Herunterladen" gebündelt aus. ## Ablauf eines Druckauftrags Der Druck erfolgt **asynchron**. Zum Zeitpunkt der Beauftragung existiert das PDF noch nicht; die serverseitige Stapelverarbeitung erzeugt es der Reihe nach. Deshalb sind Beauftragung und Entgegennahme in zwei Schritte aufgeteilt. ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → PrintJobError wird geworfen └───────┘ ``` - `print()` gibt den **Auftrags-Key** (eine Zeichenkette) zurück. Zu diesem Zeitpunkt existiert das PDF noch nicht - Der Status des Auftrags durchläuft `queued` (wartend) → `processing` (in Erzeugung) → `completed` (abgeschlossen). Bei einem Fehlschlag wird er zu `error` - `waitForCompletion()` prüft den Status wiederholt, bis er `completed` wird, und wirft bei `error` eine Exception - Erst wenn der Status `completed` ist, kann das PDF mit `downloadPdf()` abgerufen werden ## Verwendung nach Zweck ### Das PDF mit einem einzigen Aufruf entgegennehmen — `printAndDownload()` Führt Einreichen, Warten und Herunterladen gebündelt aus. **Verwenden Sie dies im Regelfall.** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` Um Wartungsintervall und Obergrenze zu ändern, geben Sie diese als fünftes Argument an. ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { intervalMs: 2000, // Intervall für die Statusprüfung timeoutMs: 90000, // bei Überschreitung: PollTimeoutError }) ``` ### Einreichen und Entgegennahme trennen — `print()` und `waitForCompletion()` Verwenden Sie dies bei einer Architektur, bei der der Auftrags-Key in einer Datenbank gespeichert und das Ergebnis später abgeholt wird. ```ts // Nur den Auftrag senden und den Schlüssel notieren const key = await client.print(workspaceKey, 'invoice.report', 'v1', data) await db.jobs.insert({ key, requestedAt: new Date() }) // ── In einem anderen Request oder einem anderen Prozess ── await client.waitForCompletion(key) const pdf = await client.downloadPdf(key) ``` ### Den Fortschritt selbst verwalten — `getStatus()` Ruft ohne Wartezeit lediglich den momentanen Status ab. Verwenden Sie dies beispielsweise, um den Fortschritt am Bildschirm anzuzeigen. ```ts const status = await client.getStatus(key) if (status.status === 'completed') { const pdf = await client.downloadPdf(key) } else if (status.status === 'error') { console.error('Druck fehlgeschlagen:', status.errorReason) } else { console.log('In Bearbeitung:', status.status) // 'queued' oder 'processing' } ``` `getStatus()` gibt lediglich den Status zurück und wirft auch bei `error` keine Exception (die Exception wird von `waitForCompletion()` geworfen). ### Große PDFs verarbeiten, ohne sie in den Speicher zu laden — `getPdfStream()` `downloadPdf()` lädt das gesamte PDF in den Speicher. Bei Berichten mit mehreren hundert Seiten ist es sicherer, den Stream entgegenzunehmen und direkt in eine Datei oder eine HTTP-Antwort zu leiten. ```ts import { Writable } from 'node:stream' import { createWriteStream } from 'node:fs' const stream = await client.getPdfStream(key) await stream.pipeTo(Writable.toWeb(createWriteStream('./invoice.pdf'))) ``` ### Das Warten vorzeitig abbrechen — `signal` Stoppt das Polling, beispielsweise wenn der Nutzer die Seite verlässt. ```ts const controller = new AbortController() // Wenn der Benutzer auf „Abbrechen“ klickt: controller.abort(new Error('Abgebrochen')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` Bei Abbruch wird das Promise mit dem an `signal.reason` übergebenen Wert abgelehnt (reject). ## Vorschau im Browser Um eine Berichtsvorschau im Browser anzuzeigen, müssen Materialien wie Vorlagen, Schriftarten und Bilder an den Browser übermittelt werden. Da dabei **das Client-Secret keinesfalls im Browser abgelegt werden darf**, ergibt sich eine dreischichtige Architektur, bei der der eigene Anwendungsserver dazwischengeschaltet wird. ``` ┌──────────────┐ ① Assets anfordern ┌────────────────┐ ② auth. Weiterleitung ┌──────────────┐ │ Browser │ ───────────────→ │ Eigener App- │ ───────────────→ │ Druck-API-Server │ │ │ │ Server │ │ │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④ Assets treffen ein │ Endpoint │ ③ Assets kommen zurück │ │ └──────────────┘ └────────────────┘ └──────────────┘ hält keine Anmeldedaten clientSecret nur hier ``` - Die Browserseite muss mit `createEndpointConnector()` lediglich **die URL der eigenen Anwendung** kennen - Die Anwendungsserver-Seite muss nur `createPreviewEndpoint()` platzieren, um mit Authentifizierung zur Druck-API weiterzuleiten - Da dieser Durchleitungs-Endpunkt genau den `PreviewConnector`-Vertrag von `tsreport-react` erfüllt, kann er direkt an die Vorschau-Komponente übergeben werden Die Strecke „Browser→Anwendung" in dieser Abbildung entspricht der eingangs genannten **Grenze A** (Sitzungsauthentifizierung der Anwendung, eigene Implementierung), die Strecke „Anwendung→Druck-API" entspricht **Grenze B** (OAuth, vom SDK verarbeitet). ### Einen Durchleitungs-Endpunkt auf dem Anwendungsserver platzieren — `createPreviewEndpoint()` Wird aus `tsreport-sdk/server` geladen (ein **reiner Server**-Unterpfad). ```ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' const client = new TsreportClient({ baseUrl: 'https://reports.example.com', clientId: 'my-client-id', clientSecret: 'my-client-secret', }) const handler = createPreviewEndpoint({ client, target: { workspace: '00000000-0000-0000-0000-000000000002', path: 'reports/invoice.report', tag: 'v1', }, }) ``` `handler` ist eine Standardfunktion vom Typ `(request: Request) => Promise`. Wird `target` angegeben, **wird stets diese Vorlage zurückgegeben, unabhängig davon, was der Browser anfragt.** Da so verhindert wird, dass der Browser beliebige Vorlagen angeben kann, wird die Angabe von `target` für öffentliche Bildschirme empfohlen (wird sie weggelassen, richtet sich der Server nach der Angabe des Browsers). > **Wichtig**: `createPreviewEndpoint()` führt keinerlei Autorisierungsprüfung durch. Die Entscheidung, „ob dieser Nutzer diesen Bericht einsehen darf", liegt in der Verantwortung der Nutzerseite. Montieren Sie den Endpunkt zwingend **innerhalb** der Authentifizierung und Autorisierung der Anwendung. ### Einbindung in Next.js App Router Umschließen Sie den Durchleitungs-Handler **zunächst mit der eigenen Authentifizierung**, bevor Sie ihn als Route-Handler exportieren. ```ts // app/api/report-preview/route.ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' import { getSessionUser } from '@/lib/auth' // ← in der App implementieren (NextAuth, iron-session, eigene Session usw.) const client = new TsreportClient({ baseUrl: process.env.REPORT_API_URL!, clientId: process.env.REPORT_CLIENT_ID!, clientSecret: process.env.REPORT_CLIENT_SECRET!, }) const previewHandler = createPreviewEndpoint({ client, target: { workspace: 'Workspace-Schlüssel', path: 'reports/invoice.report', tag: 'v1' }, }) export async function GET(request: Request): Promise { // Hier erfolgt die app-eigene Session-Authentifizierung (Grenze A). Da das SDK nichts prüft, // könnte ohne diese Prüfung jeder, der die URL erreicht, die Berichte einsehen const user = await getSessionUser(request) if (user === null) { return new Response( JSON.stringify({ message: 'unauthorized', statusCode: 401 }), { status: 401, headers: { 'content-type': 'application/json' } }, ) } return previewHandler(request) } ``` `export const GET = createPreviewEndpoint(...)` direkt zu exportieren, ist technisch zwar möglich, wird aber **nicht empfohlen, da dann kein Ort mehr für die Authentifizierung verbleibt**. Leiten Sie stets wie im obigen Beispiel erst nach der eigenen Authentifizierung weiter. Das Lesen von Umgebungsvariablen ist **Code der Nutzerseite**. Die Bibliothek selbst liest keine Umgebungsvariablen. ### Einbindung in Express — `toExpressHandler()` ```ts import express from 'express' import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint, toExpressHandler } from 'tsreport-sdk/server' const app = express() const client = new TsreportClient({ baseUrl, clientId, clientSecret }) const handler = createPreviewEndpoint({ client, target }) // requireAppUser ist die app-eigene Session-Authentifizierungs-Middleware (Grenze A, selbst implementiert). // Das Mounten hinter dieser Position bildet die Autorisierungsgrenze app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()` leitet an `next(error)` weiter, falls die Durchleitung selbst fehlschlägt (etwa weil der API-Server nicht erreichbar ist), sodass dies im Fehlerhandler der Anwendung behandelt werden kann. **Express ist keine Abhängigkeit dieses Pakets** — es wird lediglich etwas entgegengenommen, dessen Typform passt. ### Einbindung in `node:http` — `toNodeHandler()` ```ts import { createServer } from 'node:http' import { createPreviewEndpoint, toNodeHandler } from 'tsreport-sdk/server' const previewHandler = toNodeHandler(createPreviewEndpoint({ client, target })) createServer(async (req, res) => { if (req.url?.startsWith('/api/report-preview')) { // App-eigene Session-Authentifizierung (Grenze A, selbst implementiert). Darf nicht weggelassen werden if (!isAuthorizedAppUser(req)) { // ← in der App implementieren res.statusCode = 401 res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ message: 'unauthorized', statusCode: 401 })) return } try { await previewHandler(req, res) } catch (error) { res.statusCode = 500 res.setHeader('content-type', 'application/json') res.end(JSON.stringify({ message: 'preview failed', statusCode: 500 })) } return } res.statusCode = 404 res.end() }).listen(3000) ``` Das von `toNodeHandler()` zurückgegebene Promise wird abgelehnt (reject), wenn die Durchleitung selbst fehlschlägt. Fangen Sie dies wie im obigen Beispiel ab und geben Sie eine Antwort gemäß der Richtlinie Ihrer Anwendung zurück. ### Material auf der Browserseite abrufen — `createEndpointConnector()` Übergeben Sie lediglich die URL des auf dem Anwendungsserver platzierten Durchleitungs-Endpunkts. ```ts import { createEndpointConnector } from 'tsreport-sdk' const connector = createEndpointConnector({ endpoint: '/api/report-preview', fetchInit: { credentials: 'include' }, // sendet das Session-Cookie der App (für die Authentifizierung an Grenze A) }) const payload = await connector.fetchTemplate({ workspace: 'Workspace-Schlüssel', path: 'reports/invoice.report', tag: 'v1', }) // payload.template … Template-Definition // payload.fontIds … Array der Font-IDs, die dieses Template benötigt for (const fontId of payload.fontIds) { const bytes = await connector.fetchFont(fontId) // null, wenn nicht gefunden } ``` Wenn Sie den Vorlagentyp in TypeScript typisieren möchten, übergeben Sie ein Typargument. ```ts import type { ReportTemplate } from 'tsreport-core' const connector = createEndpointConnector({ endpoint: '/api/report-preview' }) ``` Der Connector weist ein **zu beachtendes Verhalten** auf: Ist `fetchTemplate()` erfolgreich, merkt er sich den Workspace und das Verzeichnis dieser Vorlage und fügt diese Informationen automatisch bei nachfolgenden Aufrufen von `resolveImage()` hinzu. Rufen Sie deshalb vor dem Auflösen von Bildern stets `fetchTemplate()` auf (bei einem Endpunkt mit fest hinterlegtem `target` ist dies nicht nötig, da die Serverseite dies ergänzt). ## Vorschaumaterial direkt serverseitig verarbeiten Material kann auch ohne den Browser direkt aus Node.js abgerufen werden. ### Die Vorlage abrufen — `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds` ist die Liste der Schriftarten, die diese Vorlage benötigt. Da der Server dies einschließlich Standardschriftarten und Schriftarten für Formeln **mit derselben Logik wie die Druck-Pipeline berechnet**, stimmen Vorschau und Druckergebnis überein, sofern Sie gemäß dieser Liste laden. ### Die Vorlage eines Unterberichts abrufen — `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` Unterscheidet sich von `getPreviewTemplate()` darin, dass kein Tag entgegengenommen wird. ### Dateien wie Bilder abrufen — `getPreviewFile()` ```ts const bytes = await client.getPreviewFile(workspaceKey, 'assets/logo.png') ``` ### Verfügbare Schriftarten auflisten — `listPreviewFonts()` ```ts const fonts = await client.listPreviewFonts() // [{ id: 'NotoSansJP', fileName: 'NotoSansJP-VariableFont_wght.ttf' }, ...] ``` ### Die Schriftartdatei selbst abrufen — `getPreviewFont()` ```ts const fontBytes = await client.getPreviewFont('NotoSansJP') ``` ### Eine eigene Durchleitung schreiben — `fetchPreviewResource()` Führt ein authentifiziertes GET auf einen beliebigen Vorschaupfad aus, falls die obigen Methoden nicht ausreichen. ```ts const response = await client.fetchPreviewResource('/api/report/preview/fonts') ``` **Nur diese Methode wirft auch im Fehlerfall keine Exception, sondern gibt die `Response` unverändert zurück.** Dies ist ein Low-Level-Einstiegspunkt für Anwendungsfälle, bei denen Statuscode und Header unverändert weitergeleitet werden sollen (genau dies nutzt `createPreviewEndpoint()`). ## Der Authentifizierungsmechanismus Die Nutzerseite muss sich hierüber keine Gedanken machen, aber intern läuft Folgendes ab. 1. Wird eine authentifizierungspflichtige Methode aufgerufen, wird geprüft, ob ein gültiges Token vorliegt 2. Falls nicht, wird per `POST /api/oauth/token` mit `grant_type=client_credentials` ein Token angefordert 3. Das erhaltene Token wird **ausschließlich im Speicher der Instanz** vorgehalten und bis zehn Sekunden vor Ablauf der Gültigkeitsdauer wiederverwendet 4. Wird eine Anfrage mit 401 oder 403 abgelehnt, wird das Token verworfen, neu beschafft und **dieselbe Anfrage genau einmal erneut gesendet** Das Token wird niemals auf Datenträgern oder in externen Speichern abgelegt. Nur wenn das Token explizit benötigt wird, verwenden Sie `getAccessToken()`. ```ts const token = await client.getAccessToken() ``` ## Umgang mit Fehlern Da alle Fehler von `TsreportClientError` erben, können sie gesammelt abgefangen oder je nach Art unterschieden werden. ```ts import { ApiError, PollTimeoutError, PrintJobError, TokenError, TsreportClientError, } from 'tsreport-sdk' try { const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) } catch (error) { if (error instanceof TokenError) { // falsche Anmeldedaten, deaktivierter Client usw. console.error('Authentifizierung fehlgeschlagen:', error.status, error.errorCode) } else if (error instanceof PrintJobError) { // die serverseitige Erzeugung ist aufgrund des Templates oder der Daten fehlgeschlagen console.error('Druckauftrag fehlgeschlagen:', error.key, error.errorReason) } else if (error instanceof PollTimeoutError) { // nicht rechtzeitig abgeschlossen (der Auftrag selbst kann noch aktiv sein) console.error('Wartezeit überschritten:', error.key, error.timeoutMs) } else if (error instanceof ApiError) { // die API hat 4xx/5xx zurückgegeben (nicht vorhandenes Template, fehlende Berechtigung usw.) console.error('API-Fehler:', error.status, error.errorMessage) } else if (error instanceof TsreportClientError) { // alles Übrige console.error(error.message) } } ``` | Fehlerklasse | Auftretender Fall | Enthaltene Informationen | | --- | --- | --- | | `TokenError` | Das Abrufen des Tokens ist fehlgeschlagen (falsche Zugangsdaten, deaktivierter Client usw.) | `status` (HTTP-Statuscode), `errorCode`, `errorDescription` | | `ApiError` | Die API hat 4xx/5xx zurückgegeben (Vorlage existiert nicht, unzureichende Berechtigung usw.) | `status`, `errorMessage` (vom Server zurückgegebene Meldung) | | `PrintJobError` | Der Auftragsstatus ist `error` geworden | `key`, `errorReason` (vom Server zurückgegebener Fehlergrund) | | `PollTimeoutError` | `waitForCompletion()` konnte den Abschluss nicht innerhalb der Zeitbegrenzung feststellen | `key`, `timeoutMs` | | `TsreportClientError` | Alle übrigen Fälle. Basisklasse aller Fehler | — | `PollTimeoutError` bedeutet lediglich, „dass der Client das Warten beendet hat", und **nicht, dass der serverseitige Auftrag fehlgeschlagen ist**. Der Key bleibt gültig, sodass später mit `getStatus()` nachgeprüft werden kann. Ausschließlich der von `createEndpointConnector()` zurückgegebene Connector gibt als Ausnahme `null` zurück, falls das Material nicht gefunden wird (404), statt eine Exception zu werfen. Der Grund ist, dass die Vorschau auch dann weiter dargestellt werden können sollte, wenn ein Teil des Materials fehlt. `fetchTemplate()` ist davon jedoch ausgenommen: Kann die Vorlage selbst nicht abgerufen werden, wird `ApiError` geworfen (da es dann nichts gibt, was dargestellt werden könnte). ## API-Referenz ### `new TsreportClient(options)` | Eigenschaft | Typ | Erforderlich | Beschreibung | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | Basis-URL des API-Servers. Ein abschließender Schrägstrich wird automatisch entfernt | | `clientId` | string | ✓ | client_id von OAuth 2.0 | | `clientSecret` | string | ✓ | client_secret von OAuth 2.0 | | `scope` | string | | Grenzt die angeforderten Scopes durch Angabe mit Leerzeichen getrennt ein. Wird sie weggelassen, werden alle für den Client registrierten Scopes gewährt | ### Methoden | Methode | Rückgabewert | Beschreibung | | --- | --- | --- | | `print(workspace, templatePath, tag, data)` | `Promise` | Beauftragt den Druck und gibt den Auftrags-Key zurück | | `getStatus(key)` | `Promise` | Gibt den aktuellen Status des Auftrags zurück (wirft keine Exception) | | `waitForCompletion(key, options?)` | `Promise` | Wartet bis zum Abschluss. Bei Fehlschlag `PrintJobError`, bei Zeitüberschreitung `PollTimeoutError` | | `downloadPdf(key)` | `Promise` | Ruft alle Bytes des PDF ab | | `getPdfStream(key)` | `Promise>` | Ruft das PDF als Stream ab | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | Führt Einreichen, Warten und Herunterladen gebündelt aus | | `getAccessToken()` | `Promise` | Gibt ein gültiges Zugriffstoken zurück (beschafft es bei Bedarf) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | Ruft die Vorlagendefinition und die benötigten Schriftart-IDs ab | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | Ruft die Vorlage eines Unterberichts ab | | `getPreviewFile(workspace, filePath)` | `Promise` | Ruft eine Datei (z. B. ein Bild) innerhalb des Workspace ab | | `listPreviewFonts()` | `Promise` | Ruft die Liste der verfügbaren Schriftarten ab | | `getPreviewFont(id)` | `Promise` | Ruft die Schriftartdatei selbst ab | | `fetchPreviewResource(resourcePath)` | `Promise` | Führt ein authentifiziertes GET auf einen beliebigen Vorschaupfad aus und gibt die rohe `Response` zurück (wirft keine Exception) | ### `WaitForCompletionOptions` | Eigenschaft | Typ | Erforderlich | Beschreibung | | --- | --- | --- | --- | | `intervalMs` | number | | Intervall zur Statusprüfung (in Millisekunden). Standard: 1000 | | `timeoutMs` | number | | Obergrenze der Wartezeit (in Millisekunden). Bei Überschreitung `PollTimeoutError`. Standard: 120000 | | `signal` | AbortSignal | | Signal zum Abbrechen des Wartens. Bei Abbruch erfolgt ein Reject mit `signal.reason` | ### Rückgabetypen | Typ | Struktur | | --- | --- | | `PrintStatusResult` | `{ key: string, status: PrintJobState, errorReason?: string }` | | `PrintJobState` | `'queued'` = wartend / `'processing'` = in Erzeugung / `'completed'` = abgeschlossen / `'error'` = fehlgeschlagen | | `PreviewTemplateResult` | `{ template: unknown, fontIds: string[] }` | | `PreviewFontInfo` | `{ id: string, fileName: string }` | ### `createEndpointConnector(options)` | Eigenschaft | Typ | Erforderlich | Beschreibung | | --- | --- | --- | --- | | `endpoint` | string | ✓ | URL, unter der der Durchleitungs-Endpunkt montiert ist (Beispiel: `/api/report-preview`) | | `fetchInit` | RequestInit | | Konfiguration, die allen Anfragen hinzugefügt wird. Zum Senden des Sitzungs-Cookies: `{ credentials: 'include' }` | Der zurückgegebene Connector besitzt die folgenden vier Methoden. Alle geben `null` zurück, falls das Material nicht gefunden wird (mit Ausnahme von `fetchTemplate()`). | Methode | Rückgabewert | Beschreibung | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | Ruft die Vorlage ab. `source` ist `{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | Ruft die Schriftartdatei selbst ab | | `resolveImage(ref)` | `Promise` | Löst ein von der Vorlage referenziertes Bild auf | | `fetchSubreportTemplate(ref, context)` | `Promise` | Ruft die Vorlage eines Unterberichts ab. `context` ist `{ workingDirectory }` | ### `tsreport-sdk/server` | Funktion | Rückgabewert | Beschreibung | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | Erstellt den Durchleitungs-Handler für Vorschaumaterial | | `toNodeHandler(handler)` | `(req, res) => Promise` | Adapter für `node:http`. Rejectet bei Fehlschlag der Durchleitung selbst | | `toExpressHandler(handler)` | `(req, res, next) => Promise` | Adapter für Express. Ein Fehlschlag der Durchleitung selbst wird an `next(error)` weitergeleitet | Optionen von `createPreviewEndpoint`: | Eigenschaft | Typ | Erforderlich | Beschreibung | | --- | --- | --- | --- | | `client` | TsreportClient | ✓ | Authentifizierter Client. Verbindungsziel und Zugangsdaten werden ausschließlich von hier übergeben | | `target` | `{ workspace, path, tag }` | | Die fest hinterlegte Vorlage. Wird sie angegeben, wird die Angabe der Anfrageseite ignoriert und stets diese Vorlage zurückgegeben. Auch das Basisverzeichnis für Bilder und Unterberichte wird ergänzt | ## Beispielcode Im Verzeichnis `examples/` sind lauffähige Implementierungsbeispiele beigefügt (auch im npm-Paket enthalten). | Datei | Inhalt | | --- | --- | | `examples/nextjs-route.ts` | Route-Handler für den Next.js App Router (mit Sitzungsauthentifizierungs-Gate) | | `examples/express-server.ts` | Einbindung in Express und Position der Autorisierungs-Middleware | | `examples/node-server.ts` | Einbindung in `node:http` | | `examples/browser-connector.ts` | Abruf von Vorschaumaterial vom Browser aus | Diese werden im Rahmen von `npm test` **tatsächlich durch Starten eines Servers geprüft**, sodass kein nicht funktionierender Code einfließen kann. ## Tests | Befehl | Inhalt | | --- | --- | | `npm test` | Unit- und Integrationstests. Einschließlich einer Prüfung, bei der `examples/` tatsächlich gestartet wird | | `npm run test:live` | Test der Zusammenarbeit mit einem echten Server, der **einen laufenden tsreport-editor und Seed-Daten voraussetzt** | ## Laufzeitumgebung - Node.js 18 oder höher (Laufzeitumgebung von `TsreportClient` und `tsreport-sdk/server`) - Moderner Browser (nur `createEndpointConnector()`. Da keinerlei Zugangsdaten vorhanden sind, kann es sicher platziert werden) - Keine Laufzeit-Abhängigkeiten - Sowohl ESM als auch CommonJS werden unterstützt ## Verwandte Projekte - [tsreport-core](https://github.com/pontasan/tsreport-core) - [tsreport-editor](https://github.com/pontasan/tsreport-editor) - [tsreport-sdk](https://github.com/pontasan/tsreport-sdk) - [tsreport-react](https://github.com/pontasan/tsreport-react) ## License tsreport-sdk kann nach Wahl der Nutzerin oder des Nutzers unter der [MIT License](./LICENSE-MIT) oder der [Apache License 2.0](./LICENSE-APACHE) verwendet werden (SPDX: `MIT OR Apache-2.0`).