# 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 | [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) **Klien untuk meminta pencetakan PDF ke server report dan menerima hasilnya. Nol dependensi paket saat runtime. Dirancang untuk digunakan di sisi server (Node.js).** tsreport-sdk menangani seluruh komunikasi dengan server API pencetakan eksternal yang disediakan oleh tsreport-editor. Autentikasi melalui OAuth 2.0, pengiriman job pencetakan, penantian hingga selesai, pengunduhan PDF, hingga pengambilan materi untuk pratinjau di browser — semuanya dapat ditangani sebagai satu kelas dengan tipe yang jelas. Karena pengambilan token, manajemen masa berlaku, dan pengambilan ulang saat kedaluwarsa diselesaikan sepenuhnya di dalam library, yang perlu ditulis oleh pengguna hanyalah "template mana yang akan diberi data apa". ## Arsitektur: Ada Dua Batas Autentikasi Sistem yang mengintegrasikan SDK ini terdiri dari 3 lapisan. **Pahami dulu gambaran besarnya.** Sebagian besar kesalahpahaman tentang SDK ini berasal dari kebingungan antara dua batas autentikasi yang ada. ```mermaid flowchart LR browser["Browser\ncreateEndpointConnector\n(tidak memiliki kredensial)"] app["Server aplikasi sendiri\ncreatePreviewEndpoint + TsreportClient\n(clientSecret hanya di sini)"] editor["Server API pencetakan\n(tsreport-editor)"] browser -->|"Batas autentikasi A: autentikasi sesi\n(diimplementasikan sendiri)"| app app -->|"Batas autentikasi B: OAuth 2.0\n(diproses otomatis oleh SDK)"| editor ``` | Lapisan | Komponen SDK yang digunakan | Peran dalam autentikasi | | --- | --- | --- | | Browser | `createEndpointConnector()` | **Pihak yang menerima autentikasi** pada Batas A. Hanya mengirim session cookie dsb., tanpa memiliki kredensial apa pun | | Server aplikasi sendiri | `createPreviewEndpoint()`+`TsreportClient` | **Pihak yang memeriksa** Batas A (pemeriksaan ini diimplementasikan sendiri oleh aplikasi). Batas B diserahkan ke SDK. Hanya lapisan ini yang memiliki `clientSecret` | | Server API pencetakan | (disediakan oleh tsreport-editor) | Memeriksa Batas B | **SDK hanya menangani Batas B.** `TsreportClient` menangani seluruh pengambilan token, manajemen masa berlaku, dan pengiriman ulang berdasarkan `clientId`/`clientSecret` secara internal. Di sisi lain, **mekanisme untuk menentukan Batas A — "siapa yang sedang mengoperasikan sistem ini, dan apakah orang tersebut berhak melihat report ini" — sama sekali tidak ada di dalam SDK ini.** `createPreviewEndpoint()` adalah relay tanpa otorisasi yang meneruskan begitu saja. Login, manajemen sesi, dan penentuan hak akses harus **selalu Anda letakkan sendiri di depan endpoint**, menggunakan mekanisme yang sudah dimiliki aplikasi Anda. Jika ini diabaikan, siapa pun yang dapat mengakses URL tersebut akan bisa membaca report dan materinya. "Pemeriksaan sesi" dan `requireAppUser` yang muncul pada contoh berikutnya bukan sekadar hiasan. **Keduanya adalah kode Batas A yang wajib diimplementasikan sendiri oleh aplikasi.** ## Apa yang Dilakukan Paket Ini, dan Apa yang Tidak Paket ini **hanya** menangani komunikasi. - **Yang dilakukan** — Autentikasi (OAuth 2.0 client credentials), pemanggilan API pencetakan, polling status job, pengambilan PDF, pengambilan materi pratinjau, penyediaan endpoint relay yang ditempatkan di server aplikasi - **Tempat berjalannya** — `TsreportClient` dan `tsreport-sdk/server` khusus untuk sisi server. Yang dapat digunakan di browser hanyalah `createEndpointConnector()`, yang tidak memiliki kredensial - **Yang tidak dilakukan** — Layout report atau pembuatan PDF (tugas `tsreport-core`), rendering tampilan pratinjau (tugas `tsreport-react`), dan **autentikasi/otorisasi pengguna** (Batas A — tugas aplikasi pengguna) Ada juga dua janji desain. **Nol dependensi paket saat runtime** (tidak ada `dependencies` di `package.json`), dan **sama sekali tidak membaca environment variable**. Tujuan koneksi maupun kredensial semuanya diterima sebagai argumen eksplisit, sehingga perilakunya tidak berubah di lingkungan mana pun. ## Instalasi ```sh npm install tsreport-sdk ``` Berjalan di Node.js 18 ke atas. Yang digunakan secara internal hanyalah `fetch` dan Web Streams, tanpa bergantung pada API khusus Node.js. **Gunakan library ini di sisi server.** Karena `TsreportClient` yang menjadi inti membutuhkan `clientSecret`, menjalankannya di browser akan menyebabkan kunci rahasia terdistribusi ke semua pengguna. Jika ingin menangani report dari browser, gunakan struktur 3 lapisan yang dijelaskan pada "Melakukan Pratinjau dari Browser" di bawah, dan di sisi browser hanya gunakan `createEndpointConnector()` yang tidak memiliki kredensial. ## Yang Diperlukan Sebelumnya Library ini tidak dapat berjalan sendiri. Prasyaratnya adalah **server API pencetakan eksternal dari tsreport-editor sedang berjalan**. Terima 4 hal berikut dari server tersebut. | Yang diperlukan | Penjelasan | | --- | --- | | Base URL | URL server API (contoh: `https://reports.example.com`). Boleh dengan atau tanpa garis miring di akhir | | Client ID | client_id OAuth 2.0 | | Client secret | client_secret OAuth 2.0. **Simpan hanya di sisi server, jangan pernah diserahkan ke browser** | | Workspace key | Identifier workspace tempat report berada (format UUID) | Client diberi scope sesuai penggunaannya. Ada 4 jenis: `report:print` (pengiriman pencetakan), `report:status` (pemeriksaan status), `report:download` (pengambilan PDF), `report:preview` (pengambilan materi pratinjau). ## Langkah Pertama: Menerima Report sebagai PDF Berikut adalah kode paling ringkas untuk mendapatkan byte PDF dengan memberikan template dan data. ```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', // Kunci workspace 'invoice.report', // Path templat di dalam workspace 'v1', // Tag templat (versi) { rows: [{ item: 'Komponen A', amount: 12000 }] }, // Data yang akan diisikan ke laporan ) writeFileSync('./invoice.pdf', pdf) ``` `printAndDownload()` menjalankan sekaligus 3 langkah yang dijelaskan berikutnya: "kirim → tunggu selesai → unduh". ## Alur Job Pencetakan Pencetakan bersifat **asinkron**. Pada saat permintaan dikirim, PDF belum ada; proses batch di sisi server yang membuatnya secara berurutan. Karena itu, permintaan dan penerimaan hasil dibagi menjadi dua langkah. ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → PrintJobError akan di-throw └───────┘ ``` - `print()` mengembalikan **key job** (string). Pada titik ini PDF belum terbentuk - Status job berpindah dari `queued` (menunggu giliran) → `processing` (sedang dibuat) → `completed` (selesai). Jika gagal, menjadi `error` - `waitForCompletion()` memeriksa status berulang kali hingga menjadi `completed`, dan melempar exception jika menjadi `error` - PDF baru dapat diambil dengan `downloadPdf()` setelah menjadi `completed` ## Penggunaan Berdasarkan Tujuan ### Ingin menerima PDF dalam satu panggilan — `printAndDownload()` Melakukan pengiriman, penantian, dan pengunduhan sekaligus. **Biasanya gunakan ini.** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` Jika ingin mengubah interval atau batas waktu penantian, tentukan pada argumen ke-5. ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { intervalMs: 2000, // Interval pengecekan status timeoutMs: 90000, // PollTimeoutError jika melebihi batas ini }) ``` ### Ingin memisahkan pengiriman dan penerimaan — `print()` dan `waitForCompletion()` Digunakan pada konfigurasi di mana key job disimpan ke database, dan hasilnya diambil belakangan. ```ts // Hanya mengirim permintaan, lalu menyimpan kuncinya const key = await client.print(workspaceKey, 'invoice.report', 'v1', data) await db.jobs.insert({ key, requestedAt: new Date() }) // ── Di request lain atau proses lain ── await client.waitForCompletion(key) const pdf = await client.downloadPdf(key) ``` ### Ingin mengelola progres sendiri — `getStatus()` Hanya mengambil status saat itu tanpa menunggu. Digunakan misalnya untuk menampilkan progres di layar. ```ts const status = await client.getStatus(key) if (status.status === 'completed') { const pdf = await client.downloadPdf(key) } else if (status.status === 'error') { console.error('Pencetakan gagal:', status.errorReason) } else { console.log('Sedang diproses:', status.status) // 'queued' atau 'processing' } ``` `getStatus()` hanya mengembalikan status dan tidak melempar exception bahkan saat `error` (yang melempar exception adalah `waitForCompletion()`). ### Ingin menangani PDF besar tanpa memuatnya ke memori — `getPdfStream()` `downloadPdf()` memuat seluruh PDF ke memori. Untuk report dengan ratusan halaman, lebih aman menerima sebagai stream dan langsung mengalirkannya ke file atau respons 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'))) ``` ### Ingin menghentikan penantian di tengah jalan — `signal` Menghentikan polling misalnya ketika pengguna meninggalkan layar. ```ts const controller = new AbortController() // Jika pengguna menekan "Batal", panggil controller.abort(new Error('Dibatalkan')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` Jika dibatalkan, Promise akan di-reject dengan nilai yang diberikan pada `signal.reason`. ## Melakukan Pratinjau dari Browser Untuk menampilkan pratinjau report di browser, materi seperti template, font, dan gambar perlu dikirim ke sisi browser. Karena **client secret tidak boleh diletakkan di browser**, digunakan struktur 3 lapisan yang menyisipkan server aplikasi sendiri di antaranya. ``` ┌──────────────┐ ① minta materi ┌────────────────┐ ② autentikasi ┌──────────────┐ │ Browser │ ───────────────→ │ Server │ ───────────────→ │ Server API │ │ │ │ aplikasi Anda │ │ cetak │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④ materi tiba │ Endpoint │ ③ materi kembali │ │ └──────────────┘ └────────────────┘ └──────────────┘ tidak memegang kredensial clientSecret hanya di sini ``` - Di sisi browser, cukup mengetahui **URL aplikasi sendiri** dengan `createEndpointConnector()` - Di sisi server aplikasi, cukup menempatkan `createPreviewEndpoint()`, dan autentikasi ditambahkan lalu diteruskan ke API pencetakan - Endpoint relay ini secara langsung memenuhi kontrak `PreviewConnector` dari `tsreport-react`, sehingga dapat diberikan langsung ke komponen pratinjau Pada diagram ini, bagian antara "Browser→Aplikasi" adalah **Batas A** (autentikasi sesi aplikasi, diimplementasikan sendiri), dan bagian "Aplikasi→API pencetakan" adalah **Batas B** (OAuth, diproses oleh SDK). ### Ingin menempatkan endpoint relay di server aplikasi — `createPreviewEndpoint()` Diimpor dari `tsreport-sdk/server` (subpath **khusus 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` adalah fungsi standar bertipe `(request: Request) => Promise`. Jika `target` ditentukan, **apa pun yang diminta dari browser akan selalu mengembalikan template ini**. Karena ini mencegah kejadian di mana sisi browser dapat menentukan template sembarangan, disarankan untuk menentukan `target` pada layar publik (jika dihilangkan, akan mengikuti penentuan dari browser). > **Penting**: `createPreviewEndpoint()` sama sekali tidak melakukan otorisasi. Penentuan "apakah pengguna ini boleh melihat report ini" adalah tanggung jawab pihak pengguna. Pastikan untuk memasangnya di **dalam** autentikasi/otorisasi aplikasi. ### Ingin mengintegrasikan dengan Next.js App Router Bungkus handler relay **dengan autentikasi sendiri terlebih dahulu**, baru ekspor sebagai route handler. ```ts // app/api/report-preview/route.ts import { TsreportClient } from 'tsreport-sdk' import { createPreviewEndpoint } from 'tsreport-sdk/server' import { getSessionUser } from '@/lib/auth' // ← diimplementasikan di aplikasi (NextAuth, iron-session, sesi buatan sendiri, dll.) 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: 'kunci workspace', path: 'reports/invoice.report', tag: 'v1' }, }) export async function GET(request: Request): Promise { // Ini adalah autentikasi sesi milik aplikasi sendiri (batas A). Karena SDK tidak memeriksa apa pun, // jika pemeriksaan ini dihilangkan, semua orang yang dapat mencapai URL bisa melihat laporan 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) } ``` Secara teknis mungkin saja mengekspor langsung dengan `export const GET = createPreviewEndpoint(...)`, tetapi **tidak disarankan karena tidak ada tempat untuk menyisipkan autentikasi**. Selalu teruskan melalui autentikasi sendiri seperti bentuk di atas. Pembacaan environment variable adalah **kode pihak pengguna**. Library itu sendiri tidak membaca environment variable. ### Ingin mengintegrasikan dengan 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 adalah middleware autentikasi sesi milik aplikasi sendiri (batas A, implementasi sendiri). // Memasangnya setelah posisi ini menjadi batas otorisasi app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()` meneruskan ke `next(error)` jika relay itu sendiri gagal (misalnya tidak dapat mencapai server API), sehingga dapat ditangani oleh error handler aplikasi. Perlu dicatat, **express bukan dependensi paket ini**. Hanya menerima sesuatu yang bentuk tipenya cocok. ### Ingin mengintegrasikan dengan `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')) { // Autentikasi sesi milik aplikasi sendiri (batas A, implementasi sendiri). Tidak boleh dihilangkan if (!isAuthorizedAppUser(req)) { // ← diimplementasikan di aplikasi 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) ``` Promise yang dikembalikan `toNodeHandler()` akan reject ketika relay itu sendiri gagal. Tangkap seperti contoh di atas, dan kembalikan respons sesuai kebijakan aplikasi. ### Ingin mengambil materi dari sisi browser — `createEndpointConnector()` Cukup berikan URL endpoint relay yang ditempatkan di server aplikasi. ```ts import { createEndpointConnector } from 'tsreport-sdk' const connector = createEndpointConnector({ endpoint: '/api/report-preview', fetchInit: { credentials: 'include' }, // Mengirim cookie sesi aplikasi (agar melewati autentikasi batas A) }) const payload = await connector.fetchTemplate({ workspace: 'kunci workspace', path: 'reports/invoice.report', tag: 'v1', }) // payload.template … definisi templat // payload.fontIds … array ID font yang dibutuhkan templat ini for (const fontId of payload.fontIds) { const bytes = await connector.fetchFont(fontId) // null jika tidak ditemukan } ``` Jika ingin memberi tipe template dengan TypeScript, berikan argumen tipe. ```ts import type { ReportTemplate } from 'tsreport-core' const connector = createEndpointConnector({ endpoint: '/api/report-preview' }) ``` Ada satu **perilaku yang perlu diingat** pada connector. Ketika `fetchTemplate()` berhasil, workspace dan direktori dari template tersebut diingat, dan secara otomatis diterapkan pada `resolveImage()` selanjutnya. Karena itu, pastikan untuk memanggil `fetchTemplate()` sebelum menyelesaikan gambar (tidak diperlukan jika endpoint memiliki `target` yang tetap, karena sisi server yang melengkapinya). ## Ingin Menangani Materi Pratinjau Langsung di Sisi Server Materi juga dapat diambil dari Node.js tanpa melalui browser. ### Ingin mengambil template — `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds` adalah daftar font yang dibutuhkan template tersebut. Karena **server menghitungnya dengan logika yang sama seperti pipeline pencetakan**, termasuk font bawaan dan font untuk rumus, memuatnya sesuai daftar ini akan membuat hasil pratinjau dan pencetakan konsisten. ### Ingin mengambil template subreport — `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` Berbeda dari `getPreviewTemplate()` dalam hal tidak mengambil tag. ### Ingin mengambil file seperti gambar — `getPreviewFile()` ```ts const bytes = await client.getPreviewFile(workspaceKey, 'assets/logo.png') ``` ### Ingin menampilkan daftar font yang tersedia — `listPreviewFonts()` ```ts const fonts = await client.listPreviewFonts() // [{ id: 'NotoSansJP', fileName: 'NotoSansJP-VariableFont_wght.ttf' }, ...] ``` ### Ingin mengambil data font itu sendiri — `getPreviewFont()` ```ts const fontBytes = await client.getPreviewFont('NotoSansJP') ``` ### Ingin menulis relay kustom sendiri — `fetchPreviewResource()` Melakukan GET dengan autentikasi ke path pratinjau apa pun, ketika metode di atas tidak mencukupi. ```ts const response = await client.fetchPreviewResource('/api/report/preview/fonts') ``` **Hanya metode ini yang mengembalikan `Response` apa adanya tanpa melempar exception bahkan saat terjadi error.** Ini adalah pintu masuk tingkat rendah untuk kebutuhan seperti meneruskan status code dan header apa adanya (yang justru digunakan oleh `createPreviewEndpoint()`). ## Mekanisme Autentikasi Pengguna tidak perlu menyadarinya, tetapi secara internal bekerja sebagai berikut. 1. Ketika metode yang memerlukan autentikasi dipanggil, diperiksa apakah ada token yang valid 2. Jika tidak ada, meminta ke `POST /api/oauth/token` dengan `grant_type=client_credentials` 3. Token yang diperoleh disimpan **hanya di dalam memori instance**, dan digunakan ulang hingga 10 detik sebelum masa berlakunya habis 4. Jika request ditolak dengan 401 atau 403, token dibuang dan diambil ulang, lalu **request yang sama dikirim ulang hanya satu kali** Token tidak pernah disimpan ke disk atau penyimpanan eksternal. Gunakan `getAccessToken()` hanya jika token diperlukan secara eksplisit. ```ts const token = await client.getAccessToken() ``` ## Penanganan Error Semua error mewarisi `TsreportClientError`, sehingga dapat ditangkap sekaligus, atau dicabangkan per jenisnya. ```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) { // Kredensial salah, klien dinonaktifkan, dll. console.error('Autentikasi gagal:', error.status, error.errorCode) } else if (error instanceof PrintJobError) { // Pembuatan di sisi server gagal karena templat atau data console.error('Job pencetakan gagal:', error.key, error.errorReason) } else if (error instanceof PollTimeoutError) { // Tidak selesai dalam batas waktu (job itu sendiri mungkin masih berjalan) console.error('Waktu tunggu habis:', error.key, error.timeoutMs) } else if (error instanceof ApiError) { // API mengembalikan 4xx/5xx (templat tidak ada, izin tidak cukup, dll.) console.error('Kesalahan API:', error.status, error.errorMessage) } else if (error instanceof TsreportClientError) { // Selain yang di atas console.error(error.message) } } ``` | Kelas error | Kondisi terjadinya | Informasi yang dimiliki | | --- | --- | --- | | `TokenError` | Gagal mengambil token (kredensial salah, client dinonaktifkan, dsb.) | `status` (status HTTP), `errorCode`, `errorDescription` | | `ApiError` | API mengembalikan 4xx/5xx (template tidak ada, hak akses tidak cukup, dsb.) | `status`, `errorMessage` (pesan yang dikembalikan server) | | `PrintJobError` | Status job menjadi `error` | `key`, `errorReason` (alasan kegagalan yang dikembalikan server) | | `PollTimeoutError` | `waitForCompletion()` tidak dapat memastikan penyelesaian dalam batas waktu | `key`, `timeoutMs` | | `TsreportClientError` | Selain di atas. Kelas dasar semua error | — | `PollTimeoutError` hanya berarti "client berhenti menunggu" dan **tidak berarti job di sisi server gagal**. Karena key-nya tetap valid, dapat diperiksa belakangan dengan `getStatus()`. Perlu dicatat, hanya connector yang dikembalikan oleh `createEndpointConnector()` yang secara khusus mengembalikan `null` alih-alih exception ketika materi tidak ditemukan (404). Ini karena pratinjau seharusnya tetap dapat dirender meskipun sebagian materi hilang. Namun `fetchTemplate()` dikecualikan; jika template itu sendiri tidak dapat diambil, `ApiError` akan dilempar (karena tidak ada apa pun yang perlu dirender). ## Referensi API ### `new TsreportClient(options)` | Properti | Tipe | Wajib | Penjelasan | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | Base URL server API. Garis miring di akhir dihapus secara otomatis | | `clientId` | string | ✓ | client_id OAuth 2.0 | | `clientSecret` | string | ✓ | client_secret OAuth 2.0 | | `scope` | string | | Mempersempit dengan menentukan scope yang diminta, dipisahkan spasi. Jika dihilangkan, semua scope yang terdaftar pada client akan diberikan | ### Metode | Metode | Nilai kembali | Penjelasan | | --- | --- | --- | | `print(workspace, templatePath, tag, data)` | `Promise` | Meminta pencetakan, dan mengembalikan key job | | `getStatus(key)` | `Promise` | Mengembalikan status job saat ini (tidak melempar exception) | | `waitForCompletion(key, options?)` | `Promise` | Menunggu hingga selesai. `PrintJobError` saat gagal, `PollTimeoutError` saat habis waktu | | `downloadPdf(key)` | `Promise` | Mengambil seluruh byte PDF | | `getPdfStream(key)` | `Promise>` | Mengambil PDF sebagai stream | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | Melakukan pengiriman, penantian, dan pengunduhan sekaligus | | `getAccessToken()` | `Promise` | Mengembalikan access token yang valid (mengambilnya jika diperlukan) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | Mengambil definisi template dan ID font yang dibutuhkan | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | Mengambil template subreport | | `getPreviewFile(workspace, filePath)` | `Promise` | Mengambil file (gambar, dsb.) di dalam workspace | | `listPreviewFonts()` | `Promise` | Mengambil daftar font yang tersedia | | `getPreviewFont(id)` | `Promise` | Mengambil data font itu sendiri | | `fetchPreviewResource(resourcePath)` | `Promise` | Melakukan GET dengan autentikasi ke path pratinjau apa pun, dan mengembalikan `Response` mentah (tidak melempar exception) | ### `WaitForCompletionOptions` | Properti | Tipe | Wajib | Penjelasan | | --- | --- | --- | --- | | `intervalMs` | number | | Interval pemeriksaan status (milidetik). Default: 1000 | | `timeoutMs` | number | | Batas atas penantian (milidetik). Melebihi ini akan menghasilkan `PollTimeoutError`. Default: 120000 | | `signal` | AbortSignal | | Sinyal untuk menghentikan penantian. Saat dihentikan, akan reject dengan `signal.reason` | ### Tipe nilai kembali | Tipe | Struktur | | --- | --- | | `PrintStatusResult` | `{ key: string, status: PrintJobState, errorReason?: string }` | | `PrintJobState` | `'queued'`=menunggu giliran / `'processing'`=sedang dibuat / `'completed'`=selesai / `'error'`=gagal | | `PreviewTemplateResult` | `{ template: unknown, fontIds: string[] }` | | `PreviewFontInfo` | `{ id: string, fileName: string }` | ### `createEndpointConnector(options)` | Properti | Tipe | Wajib | Penjelasan | | --- | --- | --- | --- | | `endpoint` | string | ✓ | URL tempat endpoint relay dipasang (contoh: `/api/report-preview`) | | `fetchInit` | RequestInit | | Konfigurasi yang diterapkan ke semua request. Gunakan `{ credentials: 'include' }` untuk mengirim session cookie | Connector yang dikembalikan memiliki 4 metode berikut. Semuanya mengembalikan `null` jika materi tidak ditemukan (kecuali `fetchTemplate()`). | Metode | Nilai kembali | Penjelasan | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | Mengambil template. `source` adalah `{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | Mengambil data font itu sendiri | | `resolveImage(ref)` | `Promise` | Menyelesaikan gambar yang direferensikan template | | `fetchSubreportTemplate(ref, context)` | `Promise` | Mengambil template subreport. `context` adalah `{ workingDirectory }` | ### `tsreport-sdk/server` | Fungsi | Nilai kembali | Penjelasan | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | Membuat handler relay untuk materi pratinjau | | `toNodeHandler(handler)` | `(req, res) => Promise` | Adapter untuk `node:http`. Reject jika relay itu sendiri gagal | | `toExpressHandler(handler)` | `(req, res, next) => Promise` | Adapter untuk Express. Kegagalan relay itu sendiri diteruskan ke `next(error)` | Opsi `createPreviewEndpoint`: | Properti | Tipe | Wajib | Penjelasan | | --- | --- | --- | --- | | `client` | TsreportClient | ✓ | Client yang sudah diautentikasi. Tujuan koneksi dan kredensial hanya diberikan dari sini | | `target` | `{ workspace, path, tag }` | | Template yang ditetapkan. Jika ditentukan, penentuan dari sisi request diabaikan, dan template ini selalu dikembalikan. Direktori dasar untuk gambar dan subreport juga dilengkapi otomatis | ## Contoh Kode Direktori `examples/` menyertakan contoh implementasi yang dapat langsung dijalankan (juga disertakan dalam paket npm). | File | Isi | | --- | --- | | `examples/nextjs-route.ts` | Route handler Next.js App Router (dengan gerbang autentikasi sesi) | | `examples/express-server.ts` | Integrasi ke Express dan posisi middleware otorisasi | | `examples/node-server.ts` | Integrasi ke `node:http` | | `examples/browser-connector.ts` | Pengambilan materi pratinjau dari browser | Karena semuanya **diverifikasi dengan benar-benar menjalankan server** di dalam `npm test`, tidak akan ada kode yang tidak berfungsi tercampur di dalamnya. ## Pengujian | Perintah | Isi | | --- | --- | | `npm test` | Uji unit dan integrasi. Termasuk verifikasi yang benar-benar menjalankan `examples/` | | `npm run test:live` | Uji integrasi server sungguhan yang **mengasumsikan tsreport-editor dan data seed sedang berjalan** | ## Lingkungan Eksekusi - Node.js 18 ke atas (lingkungan eksekusi `TsreportClient` dan `tsreport-sdk/server`) - Browser modern (hanya `createEndpointConnector()`. Karena tidak memiliki kredensial, dapat ditempatkan dengan aman) - Tanpa dependensi paket saat runtime - Mendukung ESM / CommonJS keduanya ## Proyek terkait - [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 dapat digunakan berdasarkan pilihan pengguna dengan [MIT License](./LICENSE-MIT) atau [Apache License 2.0](./LICENSE-APACHE) (SPDX: `MIT OR Apache-2.0`).