# tsreport-sdk [English](./README.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](./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のダウンロード、そしてブラウザプレビュー用の素材取得までを、型の付いた1つのクラスとして扱えます。 トークンの取得・期限管理・失効時の再取得はライブラリの内部で完結するため、利用側が書くのは「どのテンプレートに、どのデータを流すか」だけです。 ## アーキテクチャ: 認証の境界は2つある このSDKを組み込んだシステムは3つの層で構成されます。**最初に全体像を掴んでください。** このSDKに関する誤解のほとんどは、2つある認証の境界を混同することから生まれます。 ```mermaid flowchart LR browser["ブラウザ\ncreateEndpointConnector\n(資格情報を持たない)"] app["自分のアプリサーバー\ncreatePreviewEndpoint + TsreportClient\n(clientSecretはここだけ)"] editor["印字APIサーバー\n(tsreport-editor)"] browser -->|"認証の境界A: セッション認証\n(自前で実装する)"| app app -->|"認証の境界B: OAuth 2.0\n(SDKが自動処理)"| editor ``` | 層 | 使うSDKの部品 | 認証における役割 | | --- | --- | --- | | ブラウザ | `createEndpointConnector()` | 境界Aの**認証を受ける側**。セッションCookie等を送るだけで、資格情報は一切持たない | | 自分のアプリサーバー | `createPreviewEndpoint()`+`TsreportClient` | 境界Aを**検査する側**(この検査はアプリの自前実装)。境界BはSDKに任せる。`clientSecret`はこの層だけが持つ | | 印字APIサーバー | (tsreport-editorが提供) | 境界Bを検査する | **SDKが面倒を見るのは境界Bだけです。** `clientId`/`clientSecret`によるトークンの取得・期限管理・再送は`TsreportClient`がすべて内部で処理します。 一方、**境界A——「いま操作しているのは誰で、その人はこの帳票を見てよいのか」——を判断する仕組みは、このSDKのどこにも存在しません。** `createPreviewEndpoint()`は認可を一切行わない素通しの中継です。ログイン・セッション管理・権限判定は、アプリが持っている仕組みを**エンドポイントの手前に必ず自前で置いてください**。これを省くと、URLに到達できる全員が帳票と素材を読めてしまいます。 この後のサンプルに登場する「セッション確認」や`requireAppUser`は飾りではありません。**アプリ側で必ず実装が必要な、境界Aの自前コード**です。 ## 何をするパッケージで、何をしないのか このパッケージは**通信だけ**を担当します。 - **やること** — 認証(OAuth 2.0 client credentials)、印字APIの呼び出し、ジョブ状態のポーリング、PDFの取得、プレビュー素材の取得、アプリサーバーに置く中継エンドポイントの提供 - **動く場所** — `TsreportClient`と`tsreport-sdk/server`はサーバーサイド専用です。ブラウザで使えるのは、資格情報を持たない`createEndpointConnector()`だけです - **やらないこと** — 帳票のレイアウトやPDF生成(`tsreport-core`の担当)、プレビューの画面描画(`tsreport-react`の担当)、そして**利用者の認証・認可**(境界A。利用側アプリの担当) 設計上の約束も2つあります。**実行時依存パッケージはゼロ**(`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形式) | クライアントには用途に応じたスコープが割り当てられます。`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はまだ存在せず、サーバー側のバッチ処理が順番に生成します。そのため、依頼と受け取りが2つの手順に分かれています。 ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → PrintJobErrorがthrowされる └───────┘ ``` - `print()`は**ジョブのキー**(文字列)を返します。この時点ではまだPDFはできていません - ジョブの状態は`queued`(順番待ち)→ `processing`(生成中)→ `completed`(完了)と遷移します。失敗した場合は`error`になります - `waitForCompletion()`は`completed`になるまで状態を繰り返し確認し、`error`になったら例外を投げます - `completed`になって初めて`downloadPdf()`でPDFを取得できます ## 目的別の使い方 ### PDFを1回の呼び出しで受け取りたい — `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, }) ``` 中断すると、`signal.reason`に渡した値でPromiseがrejectされます。 ## ブラウザからプレビューする ブラウザで帳票プレビューを表示するには、テンプレート・フォント・画像といった素材をブラウザ側へ届ける必要があります。このとき**クライアントシークレットをブラウザに置いてはいけない**ため、間に自分のアプリサーバーを挟む3層構成になります。 ``` ┌──────────────┐ ①素材を要求 ┌────────────────┐ ②認証して転送 ┌──────────────┐ │ ブラウザ │ ───────────────→ │ 自分のアプリ │ ───────────────→ │ 印字APIサーバー │ │ │ │ サーバー │ │ │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④素材が届く │ Endpoint │ ③素材が返る │ │ └──────────────┘ └────────────────┘ └──────────────┘ 資格情報を持たない clientSecretはここだけ ``` - ブラウザ側は`createEndpointConnector()`で、**自分のアプリのURL**だけを知っていれば済みます - アプリサーバー側は`createPreviewEndpoint()`を置くだけで、認証を付けて印字APIへ中継します - この中継エンドポイントは`tsreport-react`の`PreviewConnector`契約をそのまま満たすので、プレビューコンポーネントに直接渡せます この図の「ブラウザ→アプリ」間が冒頭の**境界A**(アプリのセッション認証・自前実装)、「アプリ→印字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' }) ``` コネクタには**覚えておくべき挙動**が1つあります。`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. なければ`POST /api/oauth/token`へ`grant_type=client_credentials`で要求します 3. 取得したトークンは**インスタンスのメモリ内にのみ**保持し、有効期限の10秒前までを再利用します 4. リクエストが401または403で拒否された場合、トークンを破棄して取り直し、**同じリクエストを1度だけ再送**します トークンをディスクや外部ストアに保存することはありません。明示的にトークンが必要な場合のみ`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 | | すべてのリクエストに付与する設定。セッションcookieを送るなら`{ credentials: 'include' }` | 返されるコネクタのメソッドは次の4つです。いずれも素材が見つからない場合は`null`を返します(`fetchTemplate()`を除く)。 | メソッド | 戻り値 | 説明 | | --- | --- | --- | | `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のルートハンドラ(セッション認証ゲート付き) | | `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`)。