# 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](./README.de.md) | Français | [Español](./README.es.md) | [Português](./README.pt.md) | [العربية](./README.ar.md) | [עברית](./README.he.md) **Un client pour demander l'impression de PDF à un serveur de rapports et recevoir les résultats. Zéro dépendance en exécution. Conçu pour une utilisation côté serveur (Node.js).** tsreport-sdk prend en charge toutes les communications avec le serveur d'API d'impression externe fourni par tsreport-editor. Authentification via OAuth 2.0, soumission de tâches d'impression, attente jusqu'à leur achèvement, téléchargement du PDF, et récupération des ressources pour l'aperçu dans le navigateur : tout est géré par une seule classe typée. L'acquisition du jeton, la gestion de son expiration et sa récupération en cas d'invalidation sont entièrement internes à la bibliothèque. Il ne reste à l'utilisateur qu'à décider « quel template alimenter avec quelles données ». ## Architecture : il existe deux frontières d'authentification Un système intégrant ce SDK se compose de trois couches. **Commencez par saisir la vue d'ensemble.** La plupart des malentendus autour de ce SDK proviennent de la confusion entre les deux frontières d'authentification. ```mermaid flowchart LR browser["Navigateur\ncreateEndpointConnector\n(ne détient aucune information d'identification)"] app["Votre serveur applicatif\ncreatePreviewEndpoint + TsreportClient\n(clientSecret uniquement ici)"] editor["Serveur d'API d'impression\n(tsreport-editor)"] browser -->|"Frontière d'authentification A : authentification de session\n(à implémenter vous-même)"| app app -->|"Frontière d'authentification B : OAuth 2.0\n(gérée automatiquement par le SDK)"| editor ``` | Couche | Élément du SDK utilisé | Rôle dans l'authentification | | --- | --- | --- | | Navigateur | `createEndpointConnector()` | **Côté authentifié** de la frontière A. Envoie simplement le cookie de session, etc., et ne détient aucune information d'identification | | Votre serveur applicatif | `createPreviewEndpoint()`+`TsreportClient` | **Côté vérificateur** de la frontière A (cette vérification est une implémentation propre à l'application). La frontière B est confiée au SDK. `clientSecret` n'est détenu que par cette couche | | Serveur d'API d'impression | (fourni par tsreport-editor) | Vérifie la frontière B | **Le SDK ne s'occupe que de la frontière B.** L'acquisition, la gestion de l'expiration et le renvoi des jetons via `clientId`/`clientSecret` sont entièrement gérés en interne par `TsreportClient`. En revanche, **le mécanisme permettant de déterminer la frontière A — « qui est en train d'opérer, et cette personne est-elle autorisée à consulter ce rapport » — n'existe nulle part dans ce SDK.** `createPreviewEndpoint()` est un simple relais qui ne réalise aucune autorisation. La connexion, la gestion de session et le contrôle des permissions doivent être assurés par un mécanisme propre à l'application, **placé impérativement en amont de l'endpoint**. Si cette étape est omise, quiconque peut atteindre l'URL pourra lire les rapports et les ressources. Les mentions de « vérification de session » et de `requireAppUser` dans les exemples qui suivent ne sont pas de simples ornements. **Ce sont du code propre à l'application, à implémenter obligatoirement côté application, pour la frontière A.** ## Ce que fait ce paquet, et ce qu'il ne fait pas Ce paquet ne prend en charge **que la communication**. - **Ce qu'il fait** — l'authentification (OAuth 2.0 client credentials), l'appel de l'API d'impression, l'interrogation de l'état des tâches, la récupération du PDF, la récupération des ressources d'aperçu, et la fourniture d'un endpoint de relais à placer sur votre serveur applicatif - **Où il fonctionne** — `TsreportClient` et `tsreport-sdk/server` sont exclusivement côté serveur. Seul `createEndpointConnector()`, qui ne détient aucune information d'identification, peut être utilisé dans le navigateur - **Ce qu'il ne fait pas** — la mise en page des rapports ou la génération de PDF (rôle de `tsreport-core`), le rendu de l'aperçu à l'écran (rôle de `tsreport-react`), et **l'authentification/autorisation de l'utilisateur** (frontière A, rôle de l'application cliente) Il y a également deux engagements de conception. **Zéro dépendance en exécution** (aucun `dependencies` dans `package.json`), et **aucune lecture de variable d'environnement**. La destination de connexion et les informations d'identification sont toujours reçues sous forme d'arguments explicites, ce qui garantit un comportement identique quel que soit l'environnement de déploiement. ## Installation ```sh npm install tsreport-sdk ``` Fonctionne avec Node.js 18 ou supérieur. Seuls `fetch` et les Web Streams sont utilisés en interne ; il ne dépend d'aucune API spécifique à Node.js. **Utilisez cette bibliothèque côté serveur.** Le `TsreportClient` central nécessite `clientSecret` ; l'exécuter dans un navigateur distribuerait la clé secrète à tous les utilisateurs. Si vous souhaitez traiter les rapports depuis le navigateur, adoptez l'architecture à 3 couches décrite plus loin dans « Aperçu depuis le navigateur », en n'utilisant côté navigateur que `createEndpointConnector()`, qui ne détient aucune information d'identification. ## Prérequis Cette bibliothèque ne fonctionne pas seule. **Elle suppose qu'un serveur d'API d'impression externe tsreport-editor est en service.** Vous devez obtenir les quatre éléments suivants auprès de ce serveur. | Élément requis | Description | | --- | --- | | URL de base | URL du serveur d'API (exemple : `https://reports.example.com`). Une barre oblique finale est acceptée | | ID client | client_id OAuth 2.0 | | Secret client | client_secret OAuth 2.0. **À conserver uniquement côté serveur, jamais transmis au navigateur** | | Clé d'espace de travail | identifiant de l'espace de travail où sont placés les rapports (au format UUID) | Des portées (scopes) sont attribuées au client selon l'usage : `report:print` (soumission d'impression), `report:status` (vérification de l'état), `report:download` (récupération du PDF), `report:preview` (récupération des ressources d'aperçu), soit quatre types. ## Premier pas : recevoir un rapport sous forme de PDF Voici le code le plus court pour obtenir la séquence d'octets d'un PDF en fournissant un template et des données. ```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', // clé du workspace 'invoice.report', // chemin du modèle dans le workspace 'v1', // tag (version) du modèle { rows: [{ item: 'Pièce A', amount: 12000 }] }, // données à injecter dans le rapport ) writeFileSync('./invoice.pdf', pdf) ``` `printAndDownload()` exécute d'un seul coup les 3 étapes « soumission → attente d'achèvement → téléchargement » décrites ci-après. ## Déroulement d'une tâche d'impression L'impression est **asynchrone**. Au moment de la demande, le PDF n'existe pas encore ; il est généré séquentiellement par un traitement par lots côté serveur. C'est pourquoi la demande et la réception sont séparées en deux étapes. ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → une PrintJobError est levée └───────┘ ``` - `print()` retourne **la clé de la tâche** (une chaîne de caractères). À ce stade, le PDF n'est pas encore prêt - L'état de la tâche passe de `queued` (en attente) → `processing` (en cours de génération) → `completed` (terminé). En cas d'échec, il devient `error` - `waitForCompletion()` vérifie l'état à plusieurs reprises jusqu'à ce qu'il devienne `completed`, et lève une exception s'il devient `error` - Ce n'est qu'une fois `completed` que vous pouvez récupérer le PDF avec `downloadPdf()` ## Utilisation selon l'objectif ### Recevoir le PDF en un seul appel — `printAndDownload()` Effectue en une fois la soumission, l'attente et le téléchargement. **Utilisez ceci en temps normal.** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` Si vous souhaitez modifier l'intervalle ou la limite d'attente, indiquez-les dans le 5e argument. ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { intervalMs: 2000, // intervalle de vérification de l'état timeoutMs: 90000, // PollTimeoutError au-delà de cette durée }) ``` ### Séparer la soumission et la réception — `print()` et `waitForCompletion()` À utiliser dans une configuration où la clé de la tâche est enregistrée dans une base de données, et le résultat récupéré ultérieurement. ```ts // Envoyer uniquement la demande et conserver la clé const key = await client.print(workspaceKey, 'invoice.report', 'v1', data) await db.jobs.insert({ key, requestedAt: new Date() }) // ── Dans une autre requête ou un autre processus ── await client.waitForCompletion(key) const pdf = await client.downloadPdf(key) ``` ### Gérer soi-même la progression — `getStatus()` Récupère uniquement l'état à l'instant présent, sans attendre. Utile par exemple pour afficher la progression à l'écran. ```ts const status = await client.getStatus(key) if (status.status === 'completed') { const pdf = await client.downloadPdf(key) } else if (status.status === 'error') { console.error('Échec de l\'impression :', status.errorReason) } else { console.log('Traitement en cours :', status.status) // 'queued' ou 'processing' } ``` `getStatus()` se contente de retourner l'état, sans lever d'exception même en cas de `error` (c'est `waitForCompletion()` qui lève une exception). ### Traiter un PDF volumineux sans le charger en mémoire — `getPdfStream()` `downloadPdf()` déploie l'intégralité du PDF en mémoire. Pour des rapports de plusieurs centaines de pages, il est plus sûr de le recevoir en flux (stream) et de l'envoyer directement vers un fichier ou une réponse HTTP. ```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'))) ``` ### Interrompre l'attente en cours de route — `signal` Arrête l'interrogation (polling) lorsque, par exemple, l'utilisateur quitte l'écran. ```ts const controller = new AbortController() // Quand l'utilisateur clique sur « Annuler » : controller.abort(new Error('Annulé')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` En cas d'interruption, la promesse (Promise) est rejetée avec la valeur transmise dans `signal.reason`. ## Aperçu depuis le navigateur Pour afficher un aperçu de rapport dans le navigateur, il est nécessaire d'y acheminer des ressources telles que templates, polices et images. Or, puisque **le secret client ne doit jamais se trouver dans le navigateur**, on adopte une architecture à 3 couches faisant intervenir votre propre serveur applicatif entre les deux. ``` ┌──────────────┐ ① demande ressources ┌────────────────┐ ② authentifie, transfère ┌──────────────┐ │ Navigateur │ ───────────────→ │ Votre serveur │ ───────────────→ │ Serveur API d'impression │ │ │ │ d'application │ │ │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④ ressources reçues │ Endpoint │ ③ ressources renvoyées │ │ └──────────────┘ └────────────────┘ └──────────────┘ sans identifiants clientSecret uniquement ici ``` - Côté navigateur, il suffit de connaître **l'URL de votre propre application** grâce à `createEndpointConnector()` - Côté serveur applicatif, il suffit de placer `createPreviewEndpoint()`, qui relaie vers l'API d'impression en y ajoutant l'authentification - Cet endpoint de relais satisfait directement le contrat `PreviewConnector` de `tsreport-react`, et peut donc être transmis directement au composant d'aperçu Dans ce schéma, la liaison « navigateur → application » correspond à la **frontière A** évoquée en introduction (authentification de session de l'application, à implémenter soi-même), et « application → API d'impression » correspond à la **frontière B** (OAuth, gérée par le SDK). ### Placer un endpoint de relais sur le serveur applicatif — `createPreviewEndpoint()` À importer depuis `tsreport-sdk/server` (un sous-chemin réservé au **serveur uniquement**). ```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` est une fonction standard de type `(request: Request) => Promise`. Si vous spécifiez `target`, **quelle que soit la demande formulée par le navigateur, ce template précis sera toujours retourné**. Cela permet d'éviter l'incident où le navigateur pourrait spécifier n'importe quel template ; il est donc recommandé de spécifier `target` sur les écrans publics (si omis, le SDK suit la spécification fournie par le navigateur). > **Important** : `createPreviewEndpoint()` n'effectue aucune autorisation. La décision « cet utilisateur est-il autorisé à consulter ce rapport » relève de la responsabilité de l'application cliente. Montez-le impérativement à **l'intérieur** de l'authentification et de l'autorisation de l'application. ### Intégration avec Next.js App Router Enveloppez le gestionnaire de relais **avec votre propre authentification** avant de l'exporter comme gestionnaire de route. ```ts // app/api/report-preview/route.ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' import { getSessionUser } from '@/lib/auth' // ← à implémenter dans votre application (NextAuth, iron-session, session maison, etc.) 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: 'clé du workspace', path: 'reports/invoice.report', tag: 'v1' }, }) export async function GET(request: Request): Promise { // Ici se trouve l'authentification de session propre à l'application (frontière A). Comme le SDK ne vérifie rien, // omettre ce contrôle permettrait à quiconque atteint l'URL de consulter les rapports 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) } ``` Exporter directement `export const GET = createPreviewEndpoint(...)` est techniquement possible, mais **non recommandé, car il n'y a alors plus d'endroit où insérer l'authentification**. Passez toujours par votre propre authentification avant de relayer, comme dans la forme ci-dessus. La lecture des variables d'environnement relève du **code de l'application cliente**. La bibliothèque elle-même ne lit aucune variable d'environnement. ### Intégration avec 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 est le middleware d'authentification de session propre à l'application (frontière A, implémentation maison). // Le monter après cette position constitue la frontière d'autorisation app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()` transmet à `next(error)` si le relais lui-même échoue (par exemple si le serveur d'API est injoignable), ce qui permet un traitement par le gestionnaire d'erreurs de l'application. À noter, **express n'est pas une dépendance de ce paquet** : il se contente de recevoir un objet dont la forme de type correspond. ### Intégration avec `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')) { // Authentification de session propre à l'application (frontière A, implémentation maison). Ne pas l'omettre if (!isAuthorizedAppUser(req)) { // ← à implémenter dans votre application 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) ``` La promesse retournée par `toNodeHandler()` est rejetée lorsque le relais lui-même échoue. Comme dans l'exemple ci-dessus, interceptez-la et renvoyez une réponse conforme à la politique de votre application. ### Récupérer des ressources depuis le navigateur — `createEndpointConnector()` Il suffit de transmettre l'URL de l'endpoint de relais placé sur votre serveur applicatif. ```ts import { createEndpointConnector } from 'tsreport-sdk' const connector = createEndpointConnector({ endpoint: '/api/report-preview', fetchInit: { credentials: 'include' }, // envoie le cookie de session de l'application (pour passer l'authentification de la frontière A) }) const payload = await connector.fetchTemplate({ workspace: 'clé du workspace', path: 'reports/invoice.report', tag: 'v1', }) // payload.template … définition du modèle // payload.fontIds … tableau des ID de polices requis par ce modèle for (const fontId of payload.fontIds) { const bytes = await connector.fetchFont(fontId) // null si introuvable } ``` Si vous souhaitez typer le template en TypeScript, transmettez un paramètre de type. ```ts import type { ReportTemplate } from 'tsreport-core' const connector = createEndpointConnector({ endpoint: '/api/report-preview' }) ``` Le connecteur a **un comportement à retenir**. Lorsque `fetchTemplate()` réussit, il mémorise l'espace de travail et le répertoire de ce template, et les applique automatiquement aux appels ultérieurs de `resolveImage()`. Veillez donc à toujours appeler `fetchTemplate()` avant de résoudre des images (ce n'est pas nécessaire avec un endpoint dont `target` est fixé, car le serveur les complète lui-même). ## Manipuler directement les ressources d'aperçu côté serveur Il est également possible de récupérer les ressources depuis Node.js sans passer par le navigateur. ### Récupérer un template — `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds` est la liste des polices requises par ce template. Comme elle est **calculée par le serveur avec la même logique que le pipeline d'impression**, polices par défaut et polices mathématiques comprises, charger exactement cette liste garantit la cohérence entre l'aperçu et le résultat imprimé. ### Récupérer le template d'un sous-rapport — `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` Diffère de `getPreviewTemplate()` en ce qu'il ne prend pas de tag. ### Récupérer un fichier tel qu'une image — `getPreviewFile()` ```ts const bytes = await client.getPreviewFile(workspaceKey, 'assets/logo.png') ``` ### Lister les polices disponibles — `listPreviewFonts()` ```ts const fonts = await client.listPreviewFonts() // [{ id: 'NotoSansJP', fileName: 'NotoSansJP-VariableFont_wght.ttf' }, ...] ``` ### Récupérer l'entité d'une police — `getPreviewFont()` ```ts const fontBytes = await client.getPreviewFont('NotoSansJP') ``` ### Écrire son propre relais — `fetchPreviewResource()` Pour les cas où les méthodes ci-dessus ne suffisent pas, effectue un GET authentifié vers un chemin d'aperçu quelconque. ```ts const response = await client.fetchPreviewResource('/api/report/preview/fonts') ``` **Cette méthode, seule, retourne le `Response` tel quel sans lever d'exception, même en cas d'erreur.** Il s'agit d'un point d'entrée bas niveau destiné aux usages où l'on souhaite relayer tel quel le code de statut et les en-têtes (c'est précisément ce que `createPreviewEndpoint()` utilise). ## Fonctionnement de l'authentification L'utilisateur n'a pas besoin d'y prêter attention, mais en interne le fonctionnement est le suivant. 1. Lorsqu'une méthode nécessitant une authentification est appelée, on vérifie l'existence d'un jeton valide 2. En son absence, une demande est faite à `POST /api/oauth/token` avec `grant_type=client_credentials` 3. Le jeton obtenu est conservé **uniquement en mémoire de l'instance**, et réutilisé jusqu'à 10 secondes avant son expiration 4. Si une requête est rejetée avec un code 401 ou 403, le jeton est abandonné et redemandé, puis **la même requête est renvoyée une seule fois** Le jeton n'est jamais enregistré sur disque ni dans un magasin externe. `getAccessToken()` n'est utilisé que lorsque le jeton est explicitement nécessaire. ```ts const token = await client.getAccessToken() ``` ## Gestion des erreurs Toutes les erreurs héritent de `TsreportClientError`, ce qui permet de les capturer globalement ou de distinguer chaque type. ```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) { // identifiants incorrects, client désactivé, etc. console.error('Authentification échouée :', error.status, error.errorCode) } else if (error instanceof PrintJobError) { // la génération côté serveur a échoué à cause du modèle ou des données console.error('Le job d\'impression a échoué :', error.key, error.errorReason) } else if (error instanceof PollTimeoutError) { // non terminé dans le délai imparti (le job lui-même est peut-être encore en vie) console.error('L\'attente a expiré :', error.key, error.timeoutMs) } else if (error instanceof ApiError) { // l'API a renvoyé 4xx/5xx (modèle inexistant, permissions insuffisantes, etc.) console.error('Erreur API :', error.status, error.errorMessage) } else if (error instanceof TsreportClientError) { // autres cas console.error(error.message) } } ``` | Classe d'erreur | Situation | Informations conservées | | --- | --- | --- | | `TokenError` | Échec de l'obtention du jeton (informations d'identification incorrectes, client désactivé, etc.) | `status` (code HTTP), `errorCode`, `errorDescription` | | `ApiError` | L'API a retourné un code 4xx/5xx (template inexistant, permissions insuffisantes, etc.) | `status`, `errorMessage` (message retourné par le serveur) | | `PrintJobError` | L'état de la tâche est passé à `error` | `key`, `errorReason` (raison de l'échec retournée par le serveur) | | `PollTimeoutError` | `waitForCompletion()` n'a pas pu constater l'achèvement dans le délai imparti | `key`, `timeoutMs` | | `TsreportClientError` | Tout le reste. Classe de base de toutes les erreurs | — | `PollTimeoutError` signifie seulement que « le client a cessé d'attendre » ; **cela ne signifie pas que la tâche a échoué côté serveur**. La clé reste valide, et vous pouvez vérifier l'état ultérieurement avec `getStatus()`. À noter, seul le connecteur retourné par `createEndpointConnector()` retourne exceptionnellement `null` (au lieu de lever une exception) lorsqu'une ressource est introuvable (404). En effet, un aperçu doit pouvoir continuer à s'afficher même si une partie des ressources est manquante. Cela ne s'applique toutefois pas à `fetchTemplate()` : si le template lui-même ne peut être récupéré, une `ApiError` est levée (car il n'y aurait alors rien à afficher). ## Référence API ### `new TsreportClient(options)` | Propriété | Type | Obligatoire | Description | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | URL de base du serveur d'API. La barre oblique finale est automatiquement supprimée | | `clientId` | string | ✓ | client_id OAuth 2.0 | | `clientSecret` | string | ✓ | client_secret OAuth 2.0 | | `scope` | string | | Restreint les portées demandées, séparées par des espaces. Si omis, toutes les portées enregistrées pour le client sont accordées | ### Méthodes | Méthode | Valeur de retour | Description | | --- | --- | --- | | `print(workspace, templatePath, tag, data)` | `Promise` | Demande une impression et retourne la clé de la tâche | | `getStatus(key)` | `Promise` | Retourne l'état actuel de la tâche (ne lève pas d'exception) | | `waitForCompletion(key, options?)` | `Promise` | Attend jusqu'à l'achèvement. En cas d'échec : `PrintJobError`, en cas de délai dépassé : `PollTimeoutError` | | `downloadPdf(key)` | `Promise` | Récupère l'intégralité des octets du PDF | | `getPdfStream(key)` | `Promise>` | Récupère le PDF sous forme de flux (stream) | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | Effectue en une fois la soumission, l'attente et le téléchargement | | `getAccessToken()` | `Promise` | Retourne un jeton d'accès valide (l'obtient si nécessaire) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | Récupère la définition du template et les identifiants de police requis | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | Récupère le template d'un sous-rapport | | `getPreviewFile(workspace, filePath)` | `Promise` | Récupère un fichier (image, etc.) dans l'espace de travail | | `listPreviewFonts()` | `Promise` | Récupère la liste des polices disponibles | | `getPreviewFont(id)` | `Promise` | Récupère l'entité d'une police | | `fetchPreviewResource(resourcePath)` | `Promise` | Effectue un GET authentifié vers un chemin d'aperçu quelconque, et retourne le `Response` brut (ne lève pas d'exception) | ### `WaitForCompletionOptions` | Propriété | Type | Obligatoire | Description | | --- | --- | --- | --- | | `intervalMs` | number | | Intervalle de vérification de l'état (en millisecondes). Par défaut : 1000 | | `timeoutMs` | number | | Limite d'attente (en millisecondes). Au-delà, `PollTimeoutError`. Par défaut : 120000 | | `signal` | AbortSignal | | Signal permettant d'interrompre l'attente. En cas d'interruption, le rejet se fait avec `signal.reason` | ### Types de valeurs de retour | Type | Structure | | --- | --- | | `PrintStatusResult` | `{ key: string, status: PrintJobState, errorReason?: string }` | | `PrintJobState` | `'queued'` = en attente / `'processing'` = en cours de génération / `'completed'` = terminé / `'error'` = échec | | `PreviewTemplateResult` | `{ template: unknown, fontIds: string[] }` | | `PreviewFontInfo` | `{ id: string, fileName: string }` | ### `createEndpointConnector(options)` | Propriété | Type | Obligatoire | Description | | --- | --- | --- | --- | | `endpoint` | string | ✓ | URL sur laquelle l'endpoint de relais est monté (exemple : `/api/report-preview`) | | `fetchInit` | RequestInit | | Configuration ajoutée à toutes les requêtes. Pour envoyer le cookie de session : `{ credentials: 'include' }` | Le connecteur retourné expose les 4 méthodes suivantes. Toutes retournent `null` si la ressource est introuvable (à l'exception de `fetchTemplate()`). | Méthode | Valeur de retour | Description | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | Récupère un template. `source` est `{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | Récupère l'entité d'une police | | `resolveImage(ref)` | `Promise` | Résout une image référencée par le template | | `fetchSubreportTemplate(ref, context)` | `Promise` | Récupère le template d'un sous-rapport. `context` est `{ workingDirectory }` | ### `tsreport-sdk/server` | Fonction | Valeur de retour | Description | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | Crée un gestionnaire de relais pour les ressources d'aperçu | | `toNodeHandler(handler)` | `(req, res) => Promise` | Adaptateur pour `node:http`. Rejette en cas d'échec du relais lui-même | | `toExpressHandler(handler)` | `(req, res, next) => Promise` | Adaptateur pour Express. Transmet à `next(error)` en cas d'échec du relais lui-même | Options de `createPreviewEndpoint` : | Propriété | Type | Obligatoire | Description | | --- | --- | --- | --- | | `client` | TsreportClient | ✓ | Client authentifié. La destination de connexion et les informations d'identification ne sont transmises que par ce biais | | `target` | `{ workspace, path, tag }` | | Template à fixer. S'il est spécifié, ignore la spécification côté requête et retourne toujours ce template. Le répertoire de référence pour les images et les sous-rapports est également complété | ## Exemples de code Le répertoire `examples/` contient des exemples d'implémentation directement fonctionnels (également inclus dans le paquet npm). | Fichier | Contenu | | --- | --- | | `examples/nextjs-route.ts` | Gestionnaire de route Next.js App Router (avec verrou d'authentification de session) | | `examples/express-server.ts` | Intégration avec Express et positionnement du middleware d'autorisation | | `examples/node-server.ts` | Intégration avec `node:http` | | `examples/browser-connector.ts` | Récupération des ressources d'aperçu depuis le navigateur | Ces exemples sont **effectivement vérifiés en démarrant un serveur réel** dans `npm test`, ce qui garantit qu'aucun code non fonctionnel ne s'y glisse. ## Tests | Commande | Contenu | | --- | --- | | `npm test` | Tests unitaires et d'intégration. Inclut une vérification qui démarre réellement les fichiers de `examples/` | | `npm run test:live` | Test de communication avec un serveur réel, **présupposant un tsreport-editor en fonctionnement et des données d'amorçage (seed)** | ## Environnement d'exécution - Node.js 18 ou supérieur (environnement d'exécution de `TsreportClient` et `tsreport-sdk/server`) - Navigateur moderne (uniquement pour `createEndpointConnector()`, qui peut être placé en toute sécurité car il ne détient aucune information d'identification) - Aucune dépendance en exécution - Compatible ESM / CommonJS ## Projets associés - [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 peut être utilisé, au choix de l'utilisateur, sous [MIT License](./LICENSE-MIT) ou [Apache License 2.0](./LICENSE-APACHE) (SPDX : `MIT OR Apache-2.0`).