# 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](./README.es.md) | [Português](./README.pt.md) | [العربية](./README.ar.md) | עברית **לקוח לביצוע בקשות הדפסת PDF מול שרת הדוחות וקבלת התוצאות. ללא תלויות חבילה בזמן ריצה. מיועד לשימוש בצד השרת (Node.js).** tsreport-sdk מטפל בכל התקשורת מול שרת ה-API החיצוני להדפסה שמספק tsreport-editor. אימות באמצעות OAuth 2.0, שליחת עבודות הדפסה, המתנה להשלמתן, הורדת PDF, ואף שליפת חומרים לתצוגה מקדימה בדפדפן — כל זאת דרך מחלקה אחת עם טיפוסים מוגדרים. קבלת הטוקן, ניהול תוקפו והוצאתו מחדש במקרה של פקיעה מתבצעים כולם בתוך הספרייה עצמה, כך שכל מה שנדרש מהצד המשתמש הוא לציין "לאיזו תבנית לשלוח אילו נתונים". ## ארכיטקטורה: יש שני גבולות אימות מערכת שמשלבת SDK זה מורכבת משלוש שכבות. **חשוב להבין את התמונה הכוללת מלכתחילה.** רוב אי-ההבנות סביב 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. שולח רק Cookie של session וכדומה, ואינו מחזיק שום פרטי אימות | | שרת האפליקציה שלך | `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 להדפסה, סקירת מצב העבודה (polling), שליפת PDF, שליפת חומרים לתצוגה מקדימה, וסיפוק נקודת קצה ממסרת שממוקמת בשרת האפליקציה - **היכן היא פועלת** — `TsreportClient` ו-`tsreport-sdk/server` מיועדים לצד השרת בלבד. הרכיב היחיד שניתן להשתמש בו בדפדפן הוא `createEndpointConnector()` שאינו מחזיק פרטי אימות - **מה שהיא לא עושה** — פריסת הדוח ויצירת ה-PDF (באחריות `tsreport-core`), עיבוד מסך התצוגה המקדימה (באחריות `tsreport-react`), וכן **אימות והרשאה של המשתמש** (גבול A — באחריות אפליקציית הצד המשתמש) יש גם שתי הבטחות עיצוביות. **אין תלויות חבילה בזמן ריצה** (אין `dependencies` ב-`package.json`), ו**הספרייה אינה קוראת שום משתני סביבה**. גם יעד החיבור וגם פרטי האימות מתקבלים תמיד כארגומנטים מפורשים, כך שההתנהגות אינה משתנה בהתאם לסביבה שבה היא מותקנת. ## התקנה ```sh npm install tsreport-sdk ``` פועל תחת Node.js גרסה 18 ומעלה. בשימוש פנימי נעשה שימוש רק ב-`fetch` וב-Web Streams, ואין תלות ב-API ייחודי ל-Node.js. **יש להשתמש בספרייה זו בצד השרת בלבד.** מכיוון ש-`TsreportClient` המרכזי דורש `clientSecret`, הפעלתו בדפדפן תגרום להפצת המפתח הסודי לכל המשתמשים. אם ברצונכם לטפל בדוחות מהדפדפן, יש לבנות מבנה תלת-שכבתי כפי שמתואר בהמשך תחת "תצוגה מקדימה מהדפדפן", כאשר בצד הדפדפן נעשה שימוש רק ב-`createEndpointConnector()` שאינו מחזיק פרטי אימות. ## דרישות מוקדמות ספרייה זו אינה פועלת בעצמה. ההנחה היא ש**שרת ה-API החיצוני להדפסה של tsreport-editor פועל**. יש לקבל מאותו שרת ארבעת הפריטים הבאים. | נדרש | תיאור | | --- | --- | | כתובת בסיס (Base URL) | כתובת ה-URL של שרת ה-API (לדוגמה: `https://reports.example.com`). ניתן להוסיף לוכסן בסוף | | מזהה לקוח (Client ID) | client_id של OAuth 2.0 | | סוד לקוח (Client Secret) | client_secret של OAuth 2.0. **יש לשמור אך ורק בצד השרת ולעולם לא להעביר לדפדפן** | | מפתח סביבת עבודה (Workspace Key) | מזהה סביבת העבודה שבה נמצאים הדוחות (בפורמט UUID) | ללקוח מוקצות הרשאות (scopes) בהתאם לשימוש: `report:print` (שליחת הדפסה), `report:status` (בדיקת מצב), `report:download` (שליפת PDF), ו-`report:preview` (שליפת חומרי תצוגה מקדימה) — ארבעה סוגים בסך הכל. ## הצעד הראשון: קבלת דוח כ-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()` מבצע יחד את שלושת השלבים שיתוארו בהמשך: "שליחה → המתנה להשלמה → הורדה". ## מהלך עבודת ההדפסה ההדפסה היא **אסינכרונית**. בעת שליחת הבקשה, ה-PDF עדיין אינו קיים, ועיבוד באצווה (batch) בצד השרת יוצר אותו בתורו. לכן, השליחה והקבלה מחולקות לשני שלבים נפרדים. ``` print() downloadPdf() │ │ ▼ ▼ ┌────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │ queued │ → │ processing │ → │ completed │ → │ PDF │ └────────┘ └────────────┘ └───────────┘ └─────────┘ │ ▼ ┌───────┐ │ error │ → נזרק PrintJobError └───────┘ ``` - `print()` מחזיר **מפתח עבודה** (מחרוזת). בשלב זה ה-PDF עדיין לא נוצר - מצב העבודה עובר מ-`queued` (ממתין בתור) → `processing` (בתהליך יצירה) → `completed` (הושלם). במקרה של כישלון, המצב יהיה `error` - `waitForCompletion()` בודק את המצב שוב ושוב עד ל-`completed`, וזורק חריגה אם המצב הופך ל-`error` - רק כאשר המצב הופך ל-`completed` ניתן לקבל את ה-PDF באמצעות `downloadPdf()` ## שימוש לפי מטרה ### רוצים לקבל את ה-PDF בקריאה אחת — `printAndDownload()` מבצע יחד שליחה, המתנה והורדה. **בדרך כלל זו הפונקציה שכדאי להשתמש בה.** ```ts const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data) ``` אם ברצונכם לשנות את מרווח ההמתנה או את הגבול העליון, יש לציין זאת בארגומנט החמישי. ```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 בזיכרון. עבור דוחות בני מאות עמודים למשל, בטוח יותר לקבל stream ולהזרים אותו ישירות לקובץ או לתגובת 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` עוצר את ה-polling כאשר, לדוגמה, המשתמש עוזב את המסך. ```ts const controller = new AbortController() // כשהמשתמש לוחץ על "ביטול", מפעילים controller.abort(new Error('בוטל')) const pdf = await client.printAndDownload(workspaceKey, 'invoice.report', 'v1', data, { signal: controller.signal, }) ``` עם הביטול, ה-Promise יידחה (reject) עם הערך שהועבר ל-`signal.reason`. ## תצוגה מקדימה מהדפדפן כדי להציג תצוגה מקדימה של דוח בדפדפן, יש להעביר לדפדפן חומרים כגון תבנית, גופנים ותמונות. מכיוון **שאסור להציב את סוד הלקוח (client secret) בדפדפן**, נדרש מבנה תלת-שכבתי שבו שרת האפליקציה שלכם ממוקם באמצע. ``` ┌──────────────┐ ①בקשת חומרים ┌────────────────┐ ②אימות והעברה ┌──────────────┐ │ דפדפן │ ───────────────→ │ האפליקציה │ ───────────────→ │ שרת API להדפסה │ │ │ │ שלך: השרת │ │ │ │ createEndpoint│ ←─────────────── │ createPreview │ ←─────────────── │ │ │ Connector │ ④החומרים מגיעים │ Endpoint │ ③החומרים חוזרים │ │ └──────────────┘ └────────────────┘ └──────────────┘ ללא אישורי גישה clientSecret נמצא רק כאן ``` - בצד הדפדפן, `createEndpointConnector()` צריך להכיר רק את **כתובת ה-URL של האפליקציה שלכם** - בצד שרת האפליקציה, מספיק להציב את `createPreviewEndpoint()` כדי להעביר בקשות אל ה-API להדפסה בצירוף אימות - נקודת קצה ממסרת זו עומדת בחוזה `PreviewConnector` של `tsreport-react` ישירות, כך שניתן להעביר אותה ישירות לרכיב התצוגה המקדימה בתרשים זה, הקטע שבין "דפדפן→אפליקציה" הוא **גבול 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 יש לעטוף את פונקציית הממסר **באימות עצמי** לפני ייצואה כ-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' // ←מיישמים באפליקציה (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 הוא middleware לאימות סשן ייחודי לאפליקציה (גבול A, מימוש עצמי). // ההרכבה אחרי המיקום הזה היא שמהווה את גבול ההרשאה app.get('/api/report-preview', requireAppUser, toExpressHandler(handler)) ``` `toExpressHandler()` מעביר ל-`next(error)` במקרה שההעברה עצמה נכשלת (למשל, אין אפשרות להגיע לשרת ה-API), כך שניתן לטפל בכך ב-error handler של האפליקציה. יש לציין ש-**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) ``` ה-Promise שמחזיר `toNodeHandler()` נדחה (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 … מערך מזהי הגופנים שהתבנית הזו דורשת 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' }) ``` יש התנהגות אחת **שכדאי לזכור** בקשר ל-connector. כאשר `fetchTemplate()` מצליח, הוא זוכר את סביבת העבודה והתיקייה של אותה תבנית, ומצרף אותן אוטומטית לקריאות `resolveImage()` הבאות. לכן, יש לקרוא תמיד ל-`fetchTemplate()` לפני פענוח תמונות (אם מדובר בנקודת קצה עם `target` קבוע, הדבר מתמלא בצד השרת ואינו נחוץ). ## טיפול בחומרי תצוגה מקדימה ישירות בצד השרת ניתן גם לקבל חומרים מ-Node.js ישירות, ללא מעבר דרך הדפדפן. ### רוצים לקבל תבנית — `getPreviewTemplate()` ```ts const { template, fontIds } = await client.getPreviewTemplate(workspaceKey, 'invoice.report', 'v1') ``` `fontIds` הוא רשימת הגופנים הנדרשים לתבנית זו. מכיוון שהרשימה **מחושבת בצד השרת באמצעות אותו לוגיקה כמו צינור ההדפסה** (כולל גופני ברירת מחדל וגופנים למשוואות), טעינה לפי רשימה זו תבטיח התאמה בין התצוגה המקדימה לתוצאת ההדפסה. ### רוצים לקבל תבנית של דוח משנה (subreport) — `getPreviewSubreport()` ```ts const { template, fontIds } = await client.getPreviewSubreport(workspaceKey, 'reports/sub.report') ``` ההבדל מ-`getPreviewTemplate()` הוא שאין כאן פרמטר של תגית (tag). ### רוצים לקבל קובץ כגון תמונה — `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` כמות שהוא גם במקרה של שגיאה, ללא זריקת חריגה.** זוהי נקודת כניסה ברמה נמוכה עבור שימושים שבהם רוצים להעביר את קוד הסטטוס וה-headers כמות שהם (בדיוק כפי ש-`createPreviewEndpoint()` עושה שימוש בה). ## מנגנון האימות אין צורך שהצד המשתמש יהיה מודע לכך, אך פנימית הפעולה מתבצעת כך: 1. כאשר נקראת מתודה הדורשת אימות, נבדק האם קיים טוקן תקף 2. אם לא, נשלחת בקשה אל `POST /api/oauth/token` עם `grant_type=client_credentials` 3. הטוקן שמתקבל נשמר **רק בזיכרון של המופע (instance)**, ומשמש שוב עד 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()`. יש לציין שרק ה-connector שמוחזר על ידי `createEndpointConnector()` מהווה יוצא מן הכלל: כאשר חומר אינו נמצא (404), הוא מחזיר `null` במקום לזרוק חריגה. הסיבה היא שתצוגה מקדימה אמורה להמשיך להיות מוצגת גם כאשר חלק מהחומרים חסרים. עם זאת, `fetchTemplate()` אינו כלול בכך — אם התבנית עצמה אינה ניתנת לקבלה, נזרקת `ApiError` (מכיוון שאין דבר לצייר). ## מסמך API ### `new TsreportClient(options)` | מאפיין | טיפוס | חובה | תיאור | | --- | --- | --- | --- | | `baseUrl` | string | ✓ | כתובת הבסיס של שרת ה-API. הלוכסן בסוף מוסר אוטומטית | | `clientId` | string | ✓ | client_id של OAuth 2.0 | | `clientSecret` | string | ✓ | client_secret של OAuth 2.0 | | `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 כ-stream | | `printAndDownload(workspace, templatePath, tag, data, options?)` | `Promise` | מבצע יחד שליחה, המתנה והורדה | | `getAccessToken()` | `Promise` | מחזיר טוקן גישה תקף (מקבל חדש במידת הצורך) | | `getPreviewTemplate(workspace, templatePath, tag)` | `Promise` | מקבל את הגדרת התבנית ומזהי הגופנים הנדרשים | | `getPreviewSubreport(workspace, templatePath)` | `Promise` | מקבל את תבנית דוח המשנה (subreport) | | `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` | ### טיפוסי ערך מוחזר | טיפוס | מבנה | | --- | --- | | `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' }` | ל-connector שמוחזר יש ארבע המתודות הבאות. כולן מחזירות `null` כאשר החומר אינו נמצא (למעט `fetchTemplate()`). | מתודה | ערך מוחזר | תיאור | | --- | --- | --- | | `fetchTemplate(source)` | `Promise` | מקבל את התבנית. `source` הוא `{ workspace, path, tag }` | | `fetchFont(fontId)` | `Promise` | מקבל את גוף הגופן | | `resolveImage(ref)` | `Promise` | פותר תמונה המוזכרת בתבנית | | `fetchSubreportTemplate(ref, context)` | `Promise` | מקבל את תבנית דוח המשנה (subreport). `context` הוא `{ workingDirectory }` | ### `tsreport-sdk/server` | פונקציה | ערך מוחזר | תיאור | | --- | --- | --- | | `createPreviewEndpoint(options)` | `(request: Request) => Promise` | יוצר handler ממסר לחומרי תצוגה מקדימה | | `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` | route handler עבור Next.js App Router (עם שער אימות session) | | `examples/express-server.ts` | שילוב ב-Express ומיקום ה-middleware להרשאה | | `examples/node-server.ts` | שילוב ב-`node:http` | | `examples/browser-connector.ts` | שליפת חומרי תצוגה מקדימה מהדפדפן | דוגמאות אלה **מאומתות בפועל על ידי הפעלת שרת ממשי** בתוך `npm test`, כך שלא ייתכן שקוד שאינו פועל ייכלל בטעות. ## בדיקות | פקודה | תוכן | | --- | --- | | `npm test` | בדיקות יחידה ואינטגרציה. כוללות אימות שמפעיל בפועל את `examples/` | | `npm run test:live` | בדיקת אינטגרציה מול שרת אמיתי, **בהנחה של tsreport-editor פעיל ונתוני seed קיימים** | ## סביבת הרצה - 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`)。