# tsreport-sdk [English](./README.md) | [日本語](./README.ja.md) | [简体中文](./README.zh-CN.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](./README.es.md) | [Português](./README.pt.md) | [العربية](./README.ar.md) | [עברית](./README.he.md) **這是一個用於向報表伺服器請求PDF列印並接收結果的客戶端。執行時依賴套件為零。前提是在伺服器端(Node.js)使用。** tsreport-sdk負責處理與tsreport-editor所提供的外部列印API伺服器之間的所有通訊。從OAuth 2.0認證、列印工作的投入、等待完成、PDF下載,到瀏覽器預覽用素材的取得,全部都能以一個帶有型別的類別來處理。 由於權杖的取得、有效期限管理、失效時的重新取得都在函式庫內部完成,使用方只需要撰寫「要向哪個範本傳送哪些資料」即可。 ## 架構:認證邊界有兩個 導入此SDK的系統由3個層構成。**請先掌握整體結構。** 關於此SDK的大多數誤解,都是源自於混淆了兩個認證邊界。 ```mermaid flowchart LR browser["瀏覽器\ncreateEndpointConnector\n(不持有憑證)"] app["自己的應用程式伺服器\ncreatePreviewEndpoint + TsreportClient\n(clientSecret僅存在於此)"] editor["列印API伺服器\n(tsreport-editor)"] browser -->|"認證邊界A:Session認證\n(需自行實作)"| app app -->|"認證邊界B:OAuth 2.0\n(由SDK自動處理)"| editor ``` | 層 | 使用的SDK元件 | 在認證中的角色 | | --- | --- | --- | | 瀏覽器 | `createEndpointConnector()` | 邊界A的**受檢查方**。只需傳送Session Cookie等資訊,不持有任何憑證 | | 自己的應用程式伺服器 | `createPreviewEndpoint()`+`TsreportClient` | **檢查**邊界A的一方(此檢查由應用程式自行實作)。邊界B則交給SDK處理。`clientSecret`僅由此層持有 | | 列印API伺服器 | (由tsreport-editor提供) | 檢查邊界B | **SDK只負責照顧邊界B。** 透過`clientId`/`clientSecret`進行的權杖取得、有效期限管理、重新傳送,全部由`TsreportClient`在內部處理。 另一方面,**判斷邊界A——「現在操作的是誰,這個人是否可以查看這份報表」——的機制,並不存在於這個SDK的任何地方。** `createPreviewEndpoint()`完全不進行任何授權判斷,只是單純地轉發請求。登入、Session管理、權限判斷,都必須由應用程式已有的機制**務必自行建置在端點的前面**。若省略這一步,任何能夠存取該URL的人都能讀取到報表與素材。 在後續範例中出現的「Session確認」及`requireAppUser`並非裝飾。**它們是應用程式端必須實作的、邊界A的自行實作程式碼。** ## 這個套件做什麼、不做什麼 這個套件**只負責通訊**。 - **會做的事** — 認證(OAuth 2.0 client credentials)、呼叫列印API、輪詢工作狀態、取得PDF、取得預覽素材、提供放置於應用程式伺服器上的中繼端點 - **可運作的環境** — `TsreportClient`與`tsreport-sdk/server`僅供伺服器端使用。能在瀏覽器使用的,只有不持有憑證的`createEndpointConnector()` - **不做的事** — 報表的版面配置或PDF生成(由`tsreport-core`負責)、預覽畫面的繪製(由`tsreport-react`負責),以及**使用者的認證與授權**(邊界A,由使用方應用程式負責) 在設計上還有兩項承諾。**執行時依賴套件為零**(`package.json`中沒有`dependencies`),並且**完全不讀取環境變數**。連線目標與憑證全部以明確的參數傳入,因此無論放在哪個環境,其行為都不會改變。 ## 安裝 ```sh npm install tsreport-sdk ``` 可在Node.js 18以上的版本運作。內部僅使用`fetch`與Web Streams,不依賴Node.js特有的API。 **請在伺服器端使用此函式庫。** 由於核心的`TsreportClient`需要`clientSecret`,若在瀏覽器上執行,將會使密鑰分發給所有使用者。若想在瀏覽器上處理報表,請採用後述「從瀏覽器進行預覽」中說明的3層架構,並在瀏覽器端只使用不持有憑證的`createEndpointConnector()`。 ## 事前需準備的項目 此函式庫單獨無法運作。**前提是tsreport-editor的外部列印API伺服器必須正在運行。** 請向該伺服器取得以下4項資訊。 | 所需項目 | 說明 | | --- | --- | | 基礎URL | API伺服器的URL(例如:`https://reports.example.com`)。結尾的斜線可以有也可以沒有 | | 客戶端ID | OAuth 2.0的client_id | | 客戶端密鑰 | OAuth 2.0的client_secret。**僅在伺服器端保管,絕對不可傳遞給瀏覽器** | | 工作區金鑰 | 報表所在工作區的識別碼(UUID格式) | 客戶端會依用途被分配對應的範圍(scope)。分別為`report:print`(投入列印)、`report:status`(確認狀態)、`report:download`(取得PDF)、`report:preview`(取得預覽素材)共4種。 ## 第一步:以PDF形式接收報表 以下是傳入範本與資料以取得PDF位元組序列的最簡短程式碼。 ```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', // 工作區金鑰 'invoice.report', // 工作區內範本的路徑 'v1', // 範本的標籤(版本) { rows: [{ item: '零件A', amount: 12000 }] }, // 要填入報表的資料 ) writeFileSync('./invoice.pdf', pdf) ``` `printAndDownload()`會將後述的「投入 → 等待完成 → 下載」這3個步驟一次執行完成。 ## 列印工作的流程 列印是**非同步**的。在請求的當下,PDF尚未生成,而是由伺服器端的批次處理依序生成。因此,請求與接收分成了兩個步驟。 ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → 會擲回PrintJobError └───────┘ ``` - `print()`會回傳**工作的金鑰**(字串)。此時PDF尚未生成 - 工作的狀態會依`queued`(等待中)→ `processing`(生成中)→ `completed`(已完成)的順序轉變。若失敗則會變成`error` - `waitForCompletion()`會重複確認狀態直到變成`completed`,若變成`error`則會拋出例外 - 只有在變成`completed`之後,才能透過`downloadPdf()`取得PDF ## 依目的區分的使用方式 ### 想以一次呼叫接收PDF — `printAndDownload()` 一次完成投入、等待、下載。**一般情況下請使用此方法。** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` 若想變更等待間隔或上限,可在第5個參數中指定。 ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { intervalMs: 2000, // 確認狀態的間隔 timeoutMs: 90000, // 超過此值則發生PollTimeoutError }) ``` ### 想將投入與接收分開 — `print()` 與 `waitForCompletion()` 適用於將工作的金鑰保存於資料庫中,之後再另行取得結果的架構。 ```ts // 只送出請求,並記下金鑰 const key = await client.print(workspaceKey, 'invoice.report', 'v1', data) await db.jobs.insert({ key, requestedAt: new Date() }) // ── 在其他請求或其他行程中 ── await client.waitForCompletion(key) const pdf = await client.downloadPdf(key) ``` ### 想自行管理進度 — `getStatus()` 不進行等待,僅取得該瞬間的狀態。適用於在畫面上顯示進度等情境。 ```ts const status = await client.getStatus(key) if (status.status === 'completed') { const pdf = await client.downloadPdf(key) } else if (status.status === 'error') { console.error('列印失敗:', status.errorReason) } else { console.log('處理中:', status.status) // 'queued' 或 'processing' } ``` `getStatus()`只回傳狀態,即使是`error`也不會拋出例外(會拋出例外的是`waitForCompletion()`)。 ### 想在不將大型PDF載入記憶體的情況下處理 — `getPdfStream()` `downloadPdf()`會將整個PDF展開至記憶體中。對於數百頁的報表等情況,以串流方式接收並直接寫入檔案或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'))) ``` ### 想中途中斷等待 — `signal` 在使用者離開畫面等情況下,停止輪詢。 ```ts const controller = new AbortController() // 使用者按下「取消」時 controller.abort(new Error('已被取消')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` 中斷時,Promise會以傳入`signal.reason`的值被reject。 ## 從瀏覽器進行預覽 若要在瀏覽器中顯示報表預覽,需要將範本、字型、圖片等素材傳送至瀏覽器端。此時由於**不可將客戶端密鑰放置於瀏覽器中**,因此需要在中間夾入自己的應用程式伺服器,形成3層架構。 ``` ┌──────────────┐ ①要求素材 ┌────────────────┐ ②認證後轉送 ┌──────────────┐ │ 瀏覽器 │ ───────────────→ │ 自己的應用程式 │ ───────────────→ │ 列印API伺服器 │ │ │ │ 伺服器 │ │ │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④收到素材 │ Endpoint │ ③回傳素材 │ │ └──────────────┘ └────────────────┘ └──────────────┘ 不持有憑證 clientSecret只在這裡 ``` - 瀏覽器端只需透過`createEndpointConnector()`得知**自己應用程式的URL**即可 - 應用程式伺服器端只需放置`createPreviewEndpoint()`,即可附上認證後將請求中繼至列印API - 由於此中繼端點完全滿足`tsreport-react`的`PreviewConnector`契約,因此可以直接傳遞給預覽元件 此圖中「瀏覽器→應用程式」之間就是開頭所提到的**邊界A**(應用程式的Session認證、自行實作),「應用程式→列印API」之間則是**邊界B**(OAuth、由SDK處理)。 ### 想在應用程式伺服器上放置中繼端點 — `createPreviewEndpoint()` 從`tsreport-sdk/server`載入(**僅供伺服器使用**的子路徑)。 ```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`是一個標準的`(request: Request) => Promise`函式。 若指定`target`,**無論瀏覽器要求什麼,都必定會回傳這個範本**。由於可以防止瀏覽器端指定任意範本所造成的意外,因此在公開畫面中推薦指定`target`(若省略,則依照瀏覽器端的指定)。 > **重要**:`createPreviewEndpoint()`完全不進行任何授權判斷。「此使用者是否可以查看此報表」的判斷是使用方的責任。務必將其掛載於應用程式認證與授權的**內側**。 ### 想整合至Next.js App Router 將中繼處理器**用自己的認證包裹之後**再作為路由處理器匯出。 ```ts // app/api/report-preview/route.ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' import { getSessionUser } from '@/lib/auth' // ←由應用程式實作(NextAuth、iron-session、自建工作階段等) 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: '工作區金鑰', path: 'reports/invoice.report', tag: 'v1' }, }) export async function GET(request: Request): Promise { // 這裡是應用程式自有的工作階段認證(邊界A)。由於SDK不做任何檢查, // 若省略此檢查,所有能存取該URL的人都能檢視報表 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(...)`的方式匯出,但**由於沒有插入認證的地方,因此不建議這麼做**。請務必如上例所示,先經過自己的認證後再進行中繼。 環境變數的讀取是**使用方的程式碼**。函式庫本身不會讀取環境變數。 ### 想整合至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是應用程式自有的工作階段認證中介軟體(邊界A・自行實作)。 // 掛載在此位置之後即構成授權的邊界 app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()`會在中繼本身失敗時(例如無法連線至API伺服器等),將錯誤轉送給`next(error)`,因此可以在應用程式的錯誤處理器中處理。另外,**express並非此套件的依賴項**,只是接收符合型別形狀的物件而已。 ### 想整合至`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')) { // 應用程式自有的工作階段認證(邊界A・自行實作)。不可省略 if (!isAuthorizedAppUser(req)) { // ←由應用程式實作 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) ``` `toNodeHandler()`所回傳的Promise,會在中繼本身失敗時reject。請如上例般接住它,並依應用程式的方針回傳回應。 ### 想從瀏覽器端取得素材 — `createEndpointConnector()` 只需傳遞放置於應用程式伺服器上的中繼端點URL即可。 ```ts import { createEndpointConnector } from 'tsreport-sdk' const connector = createEndpointConnector({ endpoint: '/api/report-preview', fetchInit: { credentials: 'include' }, // 傳送應用程式的工作階段Cookie(以通過邊界A的認證) }) const payload = await connector.fetchTemplate({ workspace: '工作區金鑰', path: 'reports/invoice.report', tag: 'v1', }) // payload.template … 範本定義 // payload.fontIds … 此範本所需字型ID的陣列 for (const fontId of payload.fontIds) { const bytes = await connector.fetchFont(fontId) // 找不到則為 null } ``` 若想在TypeScript中為範本附加型別,請傳入型別參數。 ```ts import type { ReportTemplate } from 'tsreport-core' const connector = createEndpointConnector({ endpoint: '/api/report-preview' }) ``` 連接器有一個**需要記住的行為**。當`fetchTemplate()`成功後,會記住該範本的工作區與目錄,並自動附加於之後的`resolveImage()`中。因此,在解析圖片之前務必先呼叫`fetchTemplate()`(若是`target`已固定的端點,由於伺服器端會自動補齊,因此不需要)。 ## 想在伺服器端直接處理預覽素材 也可以不經過瀏覽器,直接從Node.js取得素材。 ### 想取得範本 — `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds`是該範本所需字型的清單。由於包含預設字型與數學公式用字型在內,**伺服器是以與列印管線相同的邏輯計算出來**的,因此只要依此清單載入,預覽與列印結果就會一致。 ### 想取得子報表的範本 — `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` 與`getPreviewTemplate()`的不同之處在於不接受標籤參數。 ### 想取得圖片等檔案 — `getPreviewFile()` ```ts const bytes = await client.getPreviewFile(workspaceKey, 'assets/logo.png') ``` ### 想列出可用的字型 — `listPreviewFonts()` ```ts const fonts = await client.listPreviewFonts() // [{ id: 'NotoSansJP', fileName: 'NotoSansJP-VariableFont_wght.ttf' }, ...] ``` ### 想取得字型的實體 — `getPreviewFont()` ```ts const fontBytes = await client.getPreviewFont('NotoSansJP') ``` ### 想撰寫自己的中繼邏輯 — `fetchPreviewResource()` 若上述方法不夠用時,可以對任意的預覽路徑進行附帶認證的GET請求。 ```ts const response = await client.fetchPreviewResource('/api/report/preview/fonts') ``` **只有這個方法在發生錯誤時也不會拋出例外,而是直接回傳`Response`。** 這是為了想直接中繼狀態碼與標頭等用途(`createPreviewEndpoint()`正是使用了此方法)而設的低階入口。 ## 認證機制 使用方雖不需要特別意識到,但內部是以下列方式運作的。 1. 當呼叫需要認證的方法時,會確認是否有有效的權杖 2. 若沒有,則會以`grant_type=client_credentials`向`POST /api/oauth/token`發出請求 3. 取得的權杖僅保存於**該實例的記憶體內**,並在有效期限前10秒內重複使用 4. 若請求被以401或403拒絕,則會捨棄該權杖並重新取得,並**僅重新傳送一次相同的請求** 不會將權杖保存至磁碟或外部儲存體。只有在明確需要權杖時,才使用`getAccessToken()`。 ```ts const token = await client.getAccessToken() ``` ## 錯誤處理 所有錯誤都繼承自`TsreportClientError`,因此既可以統一捕捉,也可以依種類分別處理。 ```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) { // 憑證錯誤、用戶端已被停用等 console.error('認證失敗:', error.status, error.errorCode) } else if (error instanceof PrintJobError) { // 因範本或資料的問題,伺服器端產生失敗 console.error('列印工作失敗:', error.key, error.errorReason) } else if (error instanceof PollTimeoutError) { // 未在時間內完成(工作本身可能仍在執行) console.error('等待逾時:', error.key, error.timeoutMs) } else if (error instanceof ApiError) { // API回傳了4xx/5xx(不存在的範本、權限不足等) console.error('API錯誤:', error.status, error.errorMessage) } else if (error instanceof TsreportClientError) { // 上述以外 console.error(error.message) } } ``` | 錯誤類別 | 發生的情境 | 保有的資訊 | | --- | --- | --- | | `TokenError` | 權杖取得失敗(憑證有誤、客戶端被停用等) | `status`(HTTP狀態碼)、`errorCode`、`errorDescription` | | `ApiError` | API回傳了4xx/5xx(範本不存在、權限不足等) | `status`、`errorMessage`(伺服器回傳的訊息) | | `PrintJobError` | 工作狀態變成`error` | `key`、`errorReason`(伺服器回傳的失敗原因) | | `PollTimeoutError` | `waitForCompletion()`在限制時間內未能確認完成 | `key`、`timeoutMs` | | `TsreportClientError` | 上述以外,所有錯誤的基底類別 | — | `PollTimeoutError`只是表示「客戶端停止了等待」,**並不代表伺服器端的工作失敗了**。由於金鑰依然有效,之後仍可透過`getStatus()`確認。 另外,只有`createEndpointConnector()`所回傳的連接器例外地,在找不到素材時(404)不會拋出例外,而是回傳`null`。這是因為預覽即使部分素材缺失,也應該能夠繼續繪製。不過`fetchTemplate()`不在此限,若無法取得範本本身,則會拋出`ApiError`(因為此時沒有任何可繪製的內容)。 ## API參考 ### `new TsreportClient(options)` | 屬性 | 型別 | 必填 | 說明 | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | API伺服器的基礎URL。結尾的斜線會自動移除 | | `clientId` | string | ✓ | OAuth 2.0的client_id | | `clientSecret` | string | ✓ | OAuth 2.0的client_secret | | `scope` | string | | 以空白區隔指定要求的範圍以進行篩選。若省略,則會賦予客戶端所註冊的全部範圍 | ### 方法 | 方法 | 回傳值 | 說明 | | --- | --- | --- | | `print(workspace, templatePath, tag, data)` | `Promise` | 請求列印,並回傳工作的金鑰 | | `getStatus(key)` | `Promise` | 回傳工作的目前狀態(不拋出例外) | | `waitForCompletion(key, options?)` | `Promise` | 等待直到完成。失敗時為`PrintJobError`,逾時則為`PollTimeoutError` | | `downloadPdf(key)` | `Promise` | 取得PDF的全部位元組 | | `getPdfStream(key)` | `Promise>` | 以串流方式取得PDF | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | 一次完成投入、等待、下載 | | `getAccessToken()` | `Promise` | 回傳有效的存取權杖(若有需要則會重新取得) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | 取得範本定義與所需字型ID | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | 取得子報表的範本 | | `getPreviewFile(workspace, filePath)` | `Promise` | 取得工作區內的檔案(圖片等) | | `listPreviewFonts()` | `Promise` | 取得可用字型的清單 | | `getPreviewFont(id)` | `Promise` | 取得字型的實體 | | `fetchPreviewResource(resourcePath)` | `Promise` | 對任意的預覽路徑進行附帶認證的GET請求,並回傳原始的`Response`(不拋出例外) | ### `WaitForCompletionOptions` | 屬性 | 型別 | 必填 | 說明 | | --- | --- | --- | --- | | `intervalMs` | number | | 確認狀態的間隔(毫秒)。預設值:1000 | | `timeoutMs` | number | | 等待的上限(毫秒)。超過則會拋出`PollTimeoutError`。預設值:120000 | | `signal` | AbortSignal | | 用於中斷等待的訊號。中斷時會以`signal.reason`進行reject | ### 回傳值的型別 | 型別 | 結構 | | --- | --- | | `PrintStatusResult` | `{ key: string, status: PrintJobState, errorReason?: string }` | | `PrintJobState` | `'queued'`=等待中 / `'processing'`=生成中 / `'completed'`=已完成 / `'error'`=失敗 | | `PreviewTemplateResult` | `{ template: unknown, fontIds: string[] }` | | `PreviewFontInfo` | `{ id: string, fileName: string }` | ### `createEndpointConnector(options)` | 屬性 | 型別 | 必填 | 說明 | | --- | --- | --- | --- | | `endpoint` | string | ✓ | 掛載中繼端點的URL(例如:`/api/report-preview`) | | `fetchInit` | RequestInit | | 賦予所有請求的設定。若要傳送Session cookie則設為`{ credentials: 'include' }` | 所回傳連接器的方法共有以下4個。除了`fetchTemplate()`之外,若找不到素材皆會回傳`null`。 | 方法 | 回傳值 | 說明 | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | 取得範本。`source`為`{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | 取得字型的實體 | | `resolveImage(ref)` | `Promise` | 解析範本所參照的圖片 | | `fetchSubreportTemplate(ref, context)` | `Promise` | 取得子報表的範本。`context`為`{ workingDirectory }` | ### `tsreport-sdk/server` | 函式 | 回傳值 | 說明 | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | 建立預覽素材的中繼處理器 | | `toNodeHandler(handler)` | `(req, res) => Promise` | 用於`node:http`的轉接器。中繼本身失敗時會reject | | `toExpressHandler(handler)` | `(req, res, next) => Promise` | 用於Express的轉接器。中繼本身的失敗會轉送至`next(error)` | `createPreviewEndpoint`的選項: | 屬性 | 型別 | 必填 | 說明 | | --- | --- | --- | --- | | `client` | TsreportClient | ✓ | 已完成認證的客戶端。連線目標與憑證僅由此傳入 | | `target` | `{ workspace, path, tag }` | | 要固定的範本。若指定,則會忽略請求端的指定,並始終回傳此範本。也會補齊圖片與子報表的基準目錄 | ## 範例程式碼 `examples/`目錄中同梱了可直接運作的實作範例(也包含於npm套件中)。 | 檔案 | 內容 | | --- | --- | | `examples/nextjs-route.ts` | Next.js App Router的路由處理器(附帶Session認證閘門) | | `examples/express-server.ts` | 整合至Express,以及授權中介軟體的位置 | | `examples/node-server.ts` | 整合至`node:http` | | `examples/browser-connector.ts` | 從瀏覽器取得預覽素材 | 由於這些範例在`npm test`中**實際啟動伺服器進行驗證**,因此不會混入無法運作的程式碼。 ## 測試 | 指令 | 內容 | | --- | --- | | `npm test` | 單元測試、整合測試。包含實際啟動`examples/`進行的驗證 | | `npm run test:live` | **以正在運行中的tsreport-editor與種子資料為前提**的實際伺服器連動測試 | ## 執行環境 - Node.js 18以上(`TsreportClient`與`tsreport-sdk/server`的運作環境) - 現代瀏覽器(僅限`createEndpointConnector()`。由於不持有憑證,可安全地部署) - 無執行時依賴套件 - 同時支援ESM/CommonJS ## 相關專案 - [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可依使用者的選擇,以[MIT License](./LICENSE-MIT)或[Apache License 2.0](./LICENSE-APACHE)使用(SPDX:`MIT OR Apache-2.0`)。