# 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](./README.fr.md) | Español | [Português](./README.pt.md) | [العربية](./README.ar.md) | [עברית](./README.he.md) **Un cliente para solicitar la impresión de PDF a un servidor de informes y recibir los resultados. Cero paquetes de dependencia en tiempo de ejecución. Diseñado para su uso en el lado del servidor (Node.js).** tsreport-sdk se encarga de toda la comunicación con el servidor de API de impresión externo proporcionado por tsreport-editor. La autenticación mediante OAuth 2.0, el envío de trabajos de impresión, la espera hasta su finalización, la descarga del PDF y la obtención de los recursos para la vista previa en el navegador, todo ello se maneja como una única clase tipada. La obtención de tokens, la gestión de su expiración y su renovación en caso de caducidad se resuelven completamente dentro de la biblioteca, por lo que lo único que debe escribir el usuario es "qué plantilla usar y qué datos enviarle". ## Arquitectura: hay dos límites de autenticación Un sistema que incorpora este SDK se compone de tres capas. **Primero, comprenda la panorámica general.** La mayoría de los malentendidos sobre este SDK provienen de confundir los dos límites de autenticación existentes. ```mermaid flowchart LR browser["Navegador\ncreateEndpointConnector\n(no posee credenciales)"] app["Servidor de la aplicación propia\ncreatePreviewEndpoint + TsreportClient\n(clientSecret solo aquí)"] editor["Servidor de API de impresión\n(tsreport-editor)"] browser -->|"Límite de autenticación A: autenticación de sesión\n(se implementa por cuenta propia)"| app app -->|"Límite de autenticación B: OAuth 2.0\n(el SDK lo procesa automáticamente)"| editor ``` | Capa | Componente del SDK utilizado | Rol en la autenticación | | --- | --- | --- | | Navegador | `createEndpointConnector()` | El **lado que recibe** la autenticación del límite A. Solo envía la cookie de sesión, etc., sin poseer credencial alguna | | Servidor de la aplicación propia | `createPreviewEndpoint()`+`TsreportClient` | El **lado que verifica** el límite A (esta verificación es una implementación propia de la aplicación). El límite B se delega al SDK. Solo esta capa posee `clientSecret` | | Servidor de API de impresión | (proporcionado por tsreport-editor) | Verifica el límite B | **El SDK solo se ocupa del límite B.** `TsreportClient` gestiona internamente en su totalidad la obtención, el control de expiración y el reenvío del token mediante `clientId`/`clientSecret`. Por otro lado, **el mecanismo para juzgar el límite A —quién está operando en este momento y si esa persona tiene permiso para ver este informe— no existe en ninguna parte de este SDK.** `createPreviewEndpoint()` es un simple relé que no realiza ninguna autorización. El inicio de sesión, la gestión de sesiones y la determinación de permisos deben implementarse **siempre por cuenta propia, justo antes del endpoint**, usando el mecanismo que ya posea la aplicación. Si se omite esto, cualquiera que pueda acceder a la URL podrá leer el informe y sus recursos. La "verificación de sesión" y el `requireAppUser` que aparecen en los ejemplos siguientes no son un adorno. **Es código propio del límite A que la aplicación debe implementar obligatoriamente.** ## Qué hace este paquete y qué no hace Este paquete se encarga **únicamente de la comunicación**. - **Lo que hace** — autenticación (OAuth 2.0 client credentials), llamadas a la API de impresión, sondeo del estado del trabajo, obtención del PDF, obtención de recursos para la vista previa, y provisión del endpoint de relé que se coloca en el servidor de la aplicación - **Dónde funciona** — `TsreportClient` y `tsreport-sdk/server` son exclusivos del lado del servidor. Lo único utilizable en el navegador es `createEndpointConnector()`, que no posee credenciales - **Lo que no hace** — el diseño de los informes o la generación de PDF (a cargo de `tsreport-core`), el renderizado de la pantalla de vista previa (a cargo de `tsreport-react`), y **la autenticación/autorización del usuario** (límite A, a cargo de la aplicación cliente) También existen dos compromisos de diseño. **Cero paquetes de dependencia en tiempo de ejecución** (no hay `dependencies` en `package.json`), y **no se lee ninguna variable de entorno**. Tanto el destino de conexión como las credenciales se reciben siempre como argumentos explícitos, por lo que el comportamiento no cambia sin importar en qué entorno se coloque. ## Instalación ```sh npm install tsreport-sdk ``` Funciona con Node.js 18 o superior. Internamente solo se utilizan `fetch` y Web Streams, sin depender de API específicas de Node.js. **Utilice esta biblioteca en el lado del servidor.** Dado que el `TsreportClient` central requiere `clientSecret`, ejecutarlo en el navegador distribuiría la clave secreta a todos los usuarios. Si desea manejar informes desde el navegador, adopte la arquitectura de tres capas descrita más adelante en "Vista previa desde el navegador", y utilice en el lado del navegador únicamente `createEndpointConnector()`, que no posee credenciales. ## Requisitos previos Esta biblioteca no funciona por sí sola. **Se presupone que el servidor de API de impresión externo de tsreport-editor esté en funcionamiento.** Obtenga de ese servidor los siguientes cuatro elementos. | Elemento necesario | Descripción | | --- | --- | | URL base | La URL del servidor de API (ejemplo: `https://reports.example.com`). No hay problema si tiene una barra al final | | ID de cliente | El client_id de OAuth 2.0 | | Secreto de cliente | El client_secret de OAuth 2.0. **Consérvelo únicamente en el servidor y nunca lo entregue al navegador** | | Clave del espacio de trabajo | El identificador (en formato UUID) del espacio de trabajo donde se encuentran los informes | Al cliente se le asignan ámbitos según su uso. Existen cuatro tipos: `report:print` (envío de impresión), `report:status` (verificación de estado), `report:download` (obtención del PDF) y `report:preview` (obtención de recursos de vista previa). ## Primer paso: recibir un informe como PDF Este es el código más breve para obtener la secuencia de bytes de un PDF pasando una plantilla y datos. ```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', // clave del workspace 'invoice.report', // ruta de la plantilla dentro del workspace 'v1', // tag (versión) de la plantilla { rows: [{ item: 'Pieza A', amount: 12000 }] }, // datos que se vierten en el informe ) writeFileSync('./invoice.pdf', pdf) ``` `printAndDownload()` ejecuta de una sola vez los tres pasos "enviar → esperar finalización → descargar" que se explican a continuación. ## El flujo de un trabajo de impresión La impresión es **asíncrona**. En el momento de la solicitud, el PDF aún no existe; un proceso por lotes en el servidor lo genera en orden. Por eso la solicitud y la recepción se dividen en dos pasos. ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → se lanza PrintJobError └───────┘ ``` - `print()` devuelve **la clave del trabajo** (una cadena de texto). En este momento el PDF todavía no está listo - El estado del trabajo transita de `queued` (en espera) → `processing` (generándose) → `completed` (completado). Si falla, pasa a `error` - `waitForCompletion()` verifica repetidamente el estado hasta que llega a `completed`, y lanza una excepción si llega a `error` - Solo una vez que llega a `completed` se puede obtener el PDF con `downloadPdf()` ## Uso según el propósito ### Quiero recibir el PDF con una sola llamada — `printAndDownload()` Realiza de una sola vez el envío, la espera y la descarga. **Normalmente debería usar este método.** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` Si desea cambiar el intervalo o el límite de espera, indíquelos en el quinto argumento. ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { intervalMs: 2000, // intervalo para comprobar el estado timeoutMs: 90000, // PollTimeoutError si se supera este tiempo }) ``` ### Quiero separar el envío de la recepción — `print()` y `waitForCompletion()` Se utiliza en configuraciones donde se guarda la clave del trabajo en una base de datos y el resultado se recupera más tarde. ```ts // Solo se hace la solicitud y se guarda la clave const key = await client.print(workspaceKey, 'invoice.report', 'v1', data) await db.jobs.insert({ key, requestedAt: new Date() }) // ── En otra petición u otro proceso ── await client.waitForCompletion(key) const pdf = await client.downloadPdf(key) ``` ### Quiero gestionar el progreso por mi cuenta — `getStatus()` Obtiene solo el estado en ese instante, sin esperar. Se utiliza, por ejemplo, para mostrar el progreso en pantalla. ```ts const status = await client.getStatus(key) if (status.status === 'completed') { const pdf = await client.downloadPdf(key) } else if (status.status === 'error') { console.error('La impresión falló:', status.errorReason) } else { console.log('En proceso:', status.status) // 'queued' o 'processing' } ``` `getStatus()` solo devuelve el estado y no lanza excepciones ni siquiera en `error` (la que lanza excepciones es `waitForCompletion()`). ### Quiero manejar PDF grandes sin cargarlos en memoria — `getPdfStream()` `downloadPdf()` expande el PDF completo en memoria. Para informes de cientos de páginas, es más seguro recibirlo como stream y volcarlo directamente a un archivo o a una respuesta 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'))) ``` ### Quiero interrumpir la espera a mitad de camino — `signal` Detiene el sondeo, por ejemplo, cuando el usuario abandona la pantalla. ```ts const controller = new AbortController() // Si el usuario pulsa «Cancelar», ejecutar controller.abort(new Error('Cancelado')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` Al interrumpirse, la Promise se rechaza con el valor pasado en `signal.reason`. ## Vista previa desde el navegador Para mostrar la vista previa de un informe en el navegador, es necesario entregar al navegador recursos como plantillas, fuentes e imágenes. Dado que en este caso **no se debe colocar el secreto de cliente en el navegador**, se adopta una arquitectura de tres capas en la que se interpone el servidor de la aplicación propia. ``` ┌──────────────┐ ① pide recursos ┌────────────────┐ ② autentica y reenvía ┌──────────────┐ │ Navegador │ ───────────────→ │ Tu propia app │ ───────────────→ │ Servidor API de │ │ │ │ servidor │ │ impresión │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④ llegan los recursos │ Endpoint │ ③ vuelven los recursos │ │ └──────────────┘ └────────────────┘ └──────────────┘ sin credenciales clientSecret solo aquí ``` - El lado del navegador, con `createEndpointConnector()`, solo necesita conocer **la URL de la propia aplicación** - El lado del servidor de la aplicación simplemente coloca `createPreviewEndpoint()`, que añade la autenticación y hace de relé hacia la API de impresión - Este endpoint de relé cumple tal cual con el contrato `PreviewConnector` de `tsreport-react`, por lo que puede pasarse directamente al componente de vista previa En este diagrama, el tramo "navegador→aplicación" corresponde al **límite A** mencionado al principio (autenticación de sesión de la aplicación, implementación propia), y el tramo "aplicación→API de impresión" corresponde al **límite B** (OAuth, procesado por el SDK). ### Quiero colocar un endpoint de relé en el servidor de la aplicación — `createPreviewEndpoint()` Se importa desde `tsreport-sdk/server` (una subruta **exclusiva del servidor**). ```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` es una función estándar del tipo `(request: Request) => Promise`. Si se especifica `target`, **sin importar lo que solicite el navegador, siempre se devolverá esta plantilla**. Como esto evita el accidente de que el navegador pueda especificar una plantilla arbitraria, se recomienda especificar `target` en pantallas públicas (si se omite, se sigue lo que indique el navegador). > **Importante**: `createPreviewEndpoint()` no realiza ninguna autorización. La decisión de "si este usuario puede ver este informe" es responsabilidad del lado que lo utiliza. Móntelo siempre **dentro** de la autenticación/autorización de la aplicación. ### Quiero integrarlo en Next.js App Router Envuelva el handler de relé **con su propia autenticación** antes de exportarlo como manejador de ruta. ```ts // app/api/report-preview/route.ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' import { getSessionUser } from '@/lib/auth' // ← implementar en la app (NextAuth, iron-session, sesión propia, 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: 'clave del workspace', path: 'reports/invoice.report', tag: 'v1' }, }) export async function GET(request: Request): Promise { // Aquí va la autenticación de sesión propia de la app (frontera A). Como el SDK no verifica nada, // si se omite esta verificación, cualquiera que alcance la URL podrá ver los informes 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) } ``` Es técnicamente posible exportarlo directamente con `export const GET = createPreviewEndpoint(...)`, pero **no se recomienda porque no queda ningún lugar donde interponer la autenticación**. Asegúrese siempre de pasar primero por su propia autenticación antes de retransmitir, como en el formato anterior. La lectura de variables de entorno es **código del lado que utiliza la biblioteca**. La biblioteca en sí no lee variables de entorno. ### Quiero integrarlo en 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 es el middleware de autenticación de sesión propio de la app (frontera A, implementación propia). // Montarlo después de esta posición constituye la frontera de autorización app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()` reenvía a `next(error)` cuando el relé en sí falla (por ejemplo, si no se puede alcanzar el servidor de API), de modo que pueda procesarse en el manejador de errores de la aplicación. Cabe señalar que **express no es una dependencia de este paquete**; solo recibe algo cuya forma de tipo coincida. ### Quiero integrarlo en `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')) { // Autenticación de sesión propia de la app (frontera A, implementación propia). No debe omitirse if (!isAuthorizedAppUser(req)) { // ← implementar en la app 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 Promise que devuelve `toNodeHandler()` se rechaza cuando el propio relé falla. Recíbala como en el ejemplo anterior y devuelva una respuesta acorde a la política de la aplicación. ### Quiero obtener recursos desde el lado del navegador — `createEndpointConnector()` Basta con pasar la URL del endpoint de relé colocado en el servidor de la aplicación. ```ts import { createEndpointConnector } from 'tsreport-sdk' const connector = createEndpointConnector({ endpoint: '/api/report-preview', fetchInit: { credentials: 'include' }, // enviar la cookie de sesión de la app (para pasar la autenticación de la frontera A) }) const payload = await connector.fetchTemplate({ workspace: 'clave del workspace', path: 'reports/invoice.report', tag: 'v1', }) // payload.template … definición de la plantilla // payload.fontIds … array de IDs de fuentes que necesita esta plantilla for (const fontId of payload.fontIds) { const bytes = await connector.fetchFont(fontId) // null si no se encuentra } ``` Si desea tipar la plantilla en TypeScript, pase un argumento de tipo. ```ts import type { ReportTemplate } from 'tsreport-core' const connector = createEndpointConnector({ endpoint: '/api/report-preview' }) ``` El conector tiene **un comportamiento que conviene recordar**. Cuando `fetchTemplate()` se completa con éxito, recuerda el espacio de trabajo y el directorio de esa plantilla, y los añade automáticamente a las llamadas posteriores de `resolveImage()`. Por eso, asegúrese de llamar siempre a `fetchTemplate()` antes de resolver imágenes (si se trata de un endpoint con `target` fijo, no es necesario, ya que el servidor lo completa por su cuenta). ## Quiero manejar los recursos de vista previa directamente en el servidor También es posible obtener los recursos desde Node.js sin pasar por el navegador. ### Quiero obtener una plantilla — `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds` es la lista de fuentes que requiere esa plantilla. Dado que el servidor la calcula **con la misma lógica que el pipeline de impresión**, incluyendo las fuentes predeterminadas y las de fórmulas matemáticas, si se cargan según esta lista, la vista previa coincidirá con el resultado de impresión. ### Quiero obtener la plantilla de un subinforme — `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` Se diferencia de `getPreviewTemplate()` en que no recibe una etiqueta (tag). ### Quiero obtener archivos como imágenes — `getPreviewFile()` ```ts const bytes = await client.getPreviewFile(workspaceKey, 'assets/logo.png') ``` ### Quiero listar las fuentes disponibles — `listPreviewFonts()` ```ts const fonts = await client.listPreviewFonts() // [{ id: 'NotoSansJP', fileName: 'NotoSansJP-VariableFont_wght.ttf' }, ...] ``` ### Quiero obtener el archivo real de una fuente — `getPreviewFont()` ```ts const fontBytes = await client.getPreviewFont('NotoSansJP') ``` ### Quiero escribir un relé propio — `fetchPreviewResource()` Para cuando los métodos anteriores no sean suficientes, realiza un GET autenticado a cualquier ruta de vista previa. ```ts const response = await client.fetchPreviewResource('/api/report/preview/fonts') ``` **Solo este método devuelve el `Response` tal cual, sin lanzar una excepción incluso en caso de error.** Es un punto de entrada de bajo nivel para casos en los que se desea retransmitir el código de estado y las cabeceras tal cual (precisamente `createPreviewEndpoint()` utiliza esto). ## El mecanismo de autenticación Aunque el lado que lo utiliza no necesita ser consciente de ello, internamente funciona de la siguiente manera. 1. Cuando se llama a un método que requiere autenticación, se verifica si existe un token válido 2. Si no lo hay, se solicita a `POST /api/oauth/token` con `grant_type=client_credentials` 3. El token obtenido se conserva **únicamente en la memoria de la instancia**, y se reutiliza hasta 10 segundos antes de su expiración 4. Si la solicitud es rechazada con 401 o 403, se descarta el token, se obtiene uno nuevo y **se reenvía la misma solicitud una sola vez** El token nunca se guarda en disco ni en un almacén externo. Utilice `getAccessToken()` solo cuando necesite el token de forma explícita. ```ts const token = await client.getAccessToken() ``` ## Manejo de errores Todos los errores heredan de `TsreportClientError`, por lo que pueden capturarse en conjunto o distinguirse por tipo. ```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) { // Credenciales incorrectas, cliente deshabilitado, etc. console.error('La autenticación falló:', error.status, error.errorCode) } else if (error instanceof PrintJobError) { // La generación en el servidor falló debido a la plantilla o a los datos console.error('El trabajo de impresión falló:', error.key, error.errorReason) } else if (error instanceof PollTimeoutError) { // No terminó dentro del tiempo (es posible que el trabajo en sí siga vivo) console.error('La espera agotó el tiempo:', error.key, error.timeoutMs) } else if (error instanceof ApiError) { // La API devolvió 4xx/5xx (plantilla inexistente, permisos insuficientes, etc.) console.error('Error de API:', error.status, error.errorMessage) } else if (error instanceof TsreportClientError) { // Cualquier otro caso console.error(error.message) } } ``` | Clase de error | Cuándo ocurre | Información que contiene | | --- | --- | --- | | `TokenError` | Falló la obtención del token (credenciales incorrectas, cliente deshabilitado, etc.) | `status` (estado HTTP), `errorCode`, `errorDescription` | | `ApiError` | La API devolvió un 4xx/5xx (la plantilla no existe, faltan permisos, etc.) | `status`, `errorMessage` (mensaje devuelto por el servidor) | | `PrintJobError` | El estado del trabajo pasó a `error` | `key`, `errorReason` (motivo del fallo devuelto por el servidor) | | `PollTimeoutError` | `waitForCompletion()` no pudo confirmar la finalización dentro del tiempo límite | `key`, `timeoutMs` | | `TsreportClientError` | Cualquier otro caso. Clase base de todos los errores | — | `PollTimeoutError` solo significa que "el cliente dejó de esperar" y **no significa que el trabajo en el servidor haya fallado**. La clave sigue siendo válida, por lo que puede verificarse más tarde con `getStatus()`. Cabe destacar que, de forma excepcional, solo el conector devuelto por `createEndpointConnector()` devuelve `null` en lugar de lanzar una excepción cuando no se encuentra un recurso (404). Esto se debe a que la vista previa debería poder seguir renderizándose aunque falte parte de los recursos. Sin embargo, `fetchTemplate()` está excluido de esto: si no se puede obtener la propia plantilla, se lanza un `ApiError` (porque no hay nada que renderizar). ## Referencia de la API ### `new TsreportClient(options)` | Propiedad | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | URL base del servidor de API. La barra final se elimina automáticamente | | `clientId` | string | ✓ | El client_id de OAuth 2.0 | | `clientSecret` | string | ✓ | El client_secret de OAuth 2.0 | | `scope` | string | | Restringe los ámbitos solicitados, separados por espacios. Si se omite, se otorgan todos los ámbitos registrados para el cliente | ### Métodos | Método | Valor de retorno | Descripción | | --- | --- | --- | | `print(workspace, templatePath, tag, data)` | `Promise` | Solicita la impresión y devuelve la clave del trabajo | | `getStatus(key)` | `Promise` | Devuelve el estado actual del trabajo (no lanza excepciones) | | `waitForCompletion(key, options?)` | `Promise` | Espera hasta la finalización. En caso de fallo, `PrintJobError`; si se agota el tiempo, `PollTimeoutError` | | `downloadPdf(key)` | `Promise` | Obtiene todos los bytes del PDF | | `getPdfStream(key)` | `Promise>` | Obtiene el PDF como stream | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | Realiza de una sola vez el envío, la espera y la descarga | | `getAccessToken()` | `Promise` | Devuelve un token de acceso válido (lo obtiene si es necesario) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | Obtiene la definición de la plantilla y los ID de fuente necesarios | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | Obtiene la plantilla de un subinforme | | `getPreviewFile(workspace, filePath)` | `Promise` | Obtiene un archivo (como una imagen) dentro del espacio de trabajo | | `listPreviewFonts()` | `Promise` | Obtiene la lista de fuentes disponibles | | `getPreviewFont(id)` | `Promise` | Obtiene el archivo real de una fuente | | `fetchPreviewResource(resourcePath)` | `Promise` | Realiza un GET autenticado a cualquier ruta de vista previa y devuelve el `Response` tal cual (no lanza excepciones) | ### `WaitForCompletionOptions` | Propiedad | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `intervalMs` | number | | Intervalo (en milisegundos) para verificar el estado. Por defecto: 1000 | | `timeoutMs` | number | | Límite de espera (en milisegundos). Si se supera, `PollTimeoutError`. Por defecto: 120000 | | `signal` | AbortSignal | | Señal para interrumpir la espera. Al interrumpirse, se rechaza con `signal.reason` | ### Tipos de valor de retorno | Tipo | Estructura | | --- | --- | | `PrintStatusResult` | `{ key: string, status: PrintJobState, errorReason?: string }` | | `PrintJobState` | `'queued'`=en espera / `'processing'`=generándose / `'completed'`=completado / `'error'`=fallido | | `PreviewTemplateResult` | `{ template: unknown, fontIds: string[] }` | | `PreviewFontInfo` | `{ id: string, fileName: string }` | ### `createEndpointConnector(options)` | Propiedad | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `endpoint` | string | ✓ | URL donde se ha montado el endpoint de relé (ejemplo: `/api/report-preview`) | | `fetchInit` | RequestInit | | Configuración añadida a todas las solicitudes. Para enviar la cookie de sesión, use `{ credentials: 'include' }` | Los métodos del conector devuelto son los siguientes cuatro. Todos devuelven `null` si no se encuentra el recurso (excepto `fetchTemplate()`). | Método | Valor de retorno | Descripción | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | Obtiene la plantilla. `source` es `{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | Obtiene el archivo real de una fuente | | `resolveImage(ref)` | `Promise` | Resuelve una imagen referenciada por la plantilla | | `fetchSubreportTemplate(ref, context)` | `Promise` | Obtiene la plantilla de un subinforme. `context` es `{ workingDirectory }` | ### `tsreport-sdk/server` | Función | Valor de retorno | Descripción | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | Crea el manejador de relé de recursos de vista previa | | `toNodeHandler(handler)` | `(req, res) => Promise` | Adaptador para `node:http`. Se rechaza si el relé en sí falla | | `toExpressHandler(handler)` | `(req, res, next) => Promise` | Adaptador para Express. Si el relé en sí falla, se reenvía a `next(error)` | Opciones de `createPreviewEndpoint`: | Propiedad | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `client` | TsreportClient | ✓ | Cliente ya autenticado. El destino de conexión y las credenciales solo se pasan desde aquí | | `target` | `{ workspace, path, tag }` | | La plantilla que se fija. Si se especifica, ignora lo indicado por la solicitud y siempre devuelve esta plantilla. También se completa el directorio de referencia para imágenes y subinformes | ## Código de ejemplo En el directorio `examples/` se incluyen ejemplos de implementación que funcionan tal cual (también se incluyen en el paquete npm). | Archivo | Contenido | | --- | --- | | `examples/nextjs-route.ts` | Manejador de ruta de Next.js App Router (con puerta de autenticación de sesión) | | `examples/express-server.ts` | Integración en Express y ubicación del middleware de autorización | | `examples/node-server.ts` | Integración en `node:http` | | `examples/browser-connector.ts` | Obtención de recursos de vista previa desde el navegador | Estos se verifican **iniciando realmente el servidor** dentro de `npm test`, por lo que no puede colarse código que no funcione. ## Pruebas | Comando | Contenido | | --- | --- | | `npm test` | Pruebas unitarias y de integración. Incluye la verificación que inicia realmente `examples/` | | `npm run test:live` | Pruebas de integración con un servidor real, **presuponiendo un tsreport-editor en funcionamiento y datos de semilla** | ## Entorno de ejecución - Node.js 18 o superior (entorno de funcionamiento de `TsreportClient` y `tsreport-sdk/server`) - Navegador moderno (solo `createEndpointConnector()`. Al no poseer credenciales, puede colocarse de forma segura) - Sin paquetes de dependencia en tiempo de ejecución - Compatible tanto con ESM como con CommonJS ## Proyectos relacionados - [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 puede utilizarse, a elección del usuario, bajo la [MIT License](./LICENSE-MIT) o la [Apache License 2.0](./LICENSE-APACHE) (SPDX: `MIT OR Apache-2.0`).