qorstack-report-sdk 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ This SDK simplifies integration into your **Node.js** and **.NET** applications.
6
6
  ## Features
7
7
 
8
8
  - **Word template rendering** — generate PDF or DOCX from a stored DOCX template.
9
- - **Excel template rendering** — generate XLSX (or PDF) from a stored XLSX template, with auto-filter, freeze header, table styles, and conditional formatting.
9
+ - **Excel template rendering** — generate XLSX from a stored XLSX template (Excel in, Excel out), with auto-filter, freeze header, table styles, and conditional formatting.
10
10
  - **Sandbox rendering** — render a template using its configured sample payload (great for previews).
11
11
  - **Render job history** — list and inspect past render jobs.
12
12
  - **Rich data binding:**
@@ -14,10 +14,11 @@ This SDK simplifies integration into your **Node.js** and **.NET** applications.
14
14
  - Dynamic tables (with vertical merge / column collapse on Word; auto-filter / freeze / split-to-sheets on Excel)
15
15
  - Images (URL or Base64)
16
16
  - QR codes and barcodes
17
- - **Pro features** *(requires Pro license on self-hosted, included on cloud Pro plans)*:
17
+ - **Pro features** *(require a Pro license on your Qorstack Report server)*:
18
18
  - PDF password protection
19
19
  - PDF watermarking
20
- - **Zip output** — bundle multiple output files into a `.zip`.
20
+ - Async renders with webhooks (`async: true`, then poll or `waitForJob`)
21
+ - Zip output — the rendered file inside a `.zip`
21
22
  - **Cross-platform** — fully typed clients for TypeScript/Node.js and C#/.NET 8.0+.
22
23
 
23
24
  ---
@@ -29,7 +30,7 @@ This SDK simplifies integration into your **Node.js** and **.NET** applications.
29
30
  ```bash
30
31
  npm install qorstack-report-sdk
31
32
  # or
32
- yarn add qorstack-report-sdk
33
+ pnpm add qorstack-report-sdk
33
34
  ```
34
35
 
35
36
  ### .NET (C#)
@@ -42,11 +43,11 @@ dotnet add package Qorstack.Report.Sdk
42
43
 
43
44
  ## Authentication
44
45
 
45
- You need an **API Key** from the Qorstack Portal.
46
+ You need an **API key** from your Qorstack Report server (one per project).
46
47
 
47
- 1. Log in to the Qorstack Portal.
48
- 2. Navigate to **Settings → API Keys**.
49
- 3. Create a new API Key (scoped per project).
48
+ 1. Sign in to the web UI of your Qorstack Report server.
49
+ 2. Open the project, then **API key** in the sidebar.
50
+ 3. Copy the key and send it as the `X-API-Key` header.
50
51
 
51
52
  ---
52
53
 
@@ -61,17 +62,17 @@ import { QorstackApi } from "qorstack-report-sdk";
61
62
  const qorstackApi = new QorstackApi({
62
63
  securityData: {
63
64
  headers: {
64
- "X-API-KEY": "YOUR_API_KEY_HERE",
65
+ "X-API-Key": "YOUR_API_KEY_HERE",
65
66
  },
66
67
  },
67
68
  });
68
69
 
69
70
  // OR: with custom Base URL (e.g. self-hosted instance)
70
71
  const qorstackApiCustom = new QorstackApi({
71
- baseUrl: "https://your-custom-url.com",
72
+ baseUrl: "https://your-host/api", // v2: the API lives under /api (appended automatically if missing)
72
73
  securityData: {
73
74
  headers: {
74
- "X-API-KEY": "YOUR_API_KEY_HERE",
75
+ "X-API-Key": "YOUR_API_KEY_HERE",
75
76
  },
76
77
  },
77
78
  });
@@ -86,7 +87,7 @@ using Qorstack.Report.Sdk;
86
87
  var qorstackApi = new QorstackApi(apiKey: "YOUR_API_KEY_HERE");
87
88
 
88
89
  // OR: with custom Base URL
89
- var qorstackApiCustom = new QorstackApi("https://your-custom-url.com", "YOUR_API_KEY_HERE");
90
+ var qorstackApiCustom = new QorstackApi("https://your-host/api", "YOUR_API_KEY_HERE"); // /api appended if missing
90
91
  ```
91
92
 
92
93
  ---
@@ -104,52 +105,38 @@ Use `fileType: "pdf"` (default) to get a PDF, or `"docx"` to get the rendered Wo
104
105
 
105
106
  ```typescript
106
107
  // Node.js (TypeScript)
107
- const tableRows = data.map((item, index) => ({
108
- no: index + 1,
109
- category: item.category,
110
- description: item.description,
111
- qty: item.quantity,
112
- unit_price: item.price,
113
- amount: item.total,
114
- }));
108
+ // Column names ({{row:<name>}} markers) are listed once; each row holds the values in that order
109
+ const columns = ["no", "category", "description", "qty", "unit_price", "amount"];
110
+ const tableRows = data.map((item, index) => [
111
+ index + 1, item.category, item.description, item.quantity, item.price, item.total,
112
+ ]);
115
113
 
116
114
  const response = await qorstackApi.render.postRenderWordTemplate({
117
115
  templateKey: "RENDOX-7326-758-974-2067",
118
116
  fileName: "Full_Report_Example",
119
117
  fileType: "pdf", // "pdf" | "docx"
120
- replace: {
121
- customer_name: "Example Company Ltd.",
122
- document_date: new Date().toLocaleDateString("en-GB"),
123
- total_amount: "1,500.00",
124
- },
118
+ replace: [
119
+ { key: "customer_name", value: "Example Company Ltd." },
120
+ { key: "document_date", value: new Date().toLocaleDateString("en-GB") },
121
+ { key: "total_amount", value: "1,500.00" },
122
+ ],
125
123
  table: [
126
124
  {
125
+ columns,
127
126
  rows: tableRows,
128
127
  verticalMerge: ["category"],
129
128
  collapse: ["description"],
130
129
  },
131
130
  ],
132
- image: {
133
- header_logo: {
134
- src: "https://example.com/logo.png",
135
- width: 200,
136
- fit: "contain",
137
- },
138
- },
139
- qrcode: {
140
- payment_qr: {
141
- text: "https://payment.link/123456",
142
- size: 150,
143
- },
144
- },
145
- barcode: {
146
- product_barcode: {
147
- text: "PROD-123456789",
148
- format: "Code128",
149
- width: 300,
150
- height: 80,
151
- },
152
- },
131
+ image: [
132
+ { key: "header_logo", src: "https://example.com/logo.png", width: 200, fit: "contain" },
133
+ ],
134
+ qrcode: [
135
+ { key: "payment_qr", text: "https://payment.link/123456", size: 150 },
136
+ ],
137
+ barcode: [
138
+ { key: "product_barcode", text: "PROD-123456789", format: "Code128", width: 300, height: 80 },
139
+ ],
153
140
  });
154
141
 
155
142
  console.log("Download URL:", response.data.downloadUrl);
@@ -159,47 +146,45 @@ console.log("Download URL:", response.data.downloadUrl);
159
146
  // C# (.NET)
160
147
  var data = await _exampleService.GetDataAsync(id);
161
148
 
162
- var tableRows = data.Select((item, index) => new Dictionary<string, object>
149
+ // Column names ({{row:<name>}} markers) are listed once; each row holds the values in that order
150
+ var columns = new List<string> { "no", "category", "description", "qty", "unit_price", "amount" };
151
+ var tableRows = data.Select((item, index) => (ICollection<object>)new List<object>
163
152
  {
164
- { "no", index + 1 },
165
- { "category", item.Category },
166
- { "description", item.Description },
167
- { "qty", item.Quantity },
168
- { "unit_price", item.Price },
169
- { "amount", item.Total }
153
+ index + 1, item.Category, item.Description, item.Quantity, item.Price, item.Total
170
154
  }).ToList();
171
155
 
172
156
  var request = new PdfFromTemplateRequest
173
157
  {
174
158
  TemplateKey = "RENDOX-7326-758-974-2067",
175
159
  FileName = "Full_Report_Example",
176
- FileType = "pdf",
177
- Replace = new()
160
+ FileType = PdfFromTemplateRequestFileType.Pdf,
161
+ Replace = new List<KeyValueRequest>
178
162
  {
179
- { "customer_name", "Example Company Ltd." },
180
- { "document_date", DateTime.Now.ToString("dd/MM/yyyy") },
181
- { "total_amount", "1,500.00" }
163
+ new KeyValueRequest { Key = "customer_name", Value = "Example Company Ltd." },
164
+ new KeyValueRequest { Key = "document_date", Value = DateTime.Now.ToString("dd/MM/yyyy") },
165
+ new KeyValueRequest { Key = "total_amount", Value = "1,500.00" }
182
166
  },
183
167
  Table = new()
184
168
  {
185
169
  new()
186
170
  {
187
- Rows = tableRows,
171
+ Columns = columns,
172
+ Rows = tableRows,
188
173
  VerticalMerge = new() { "category" },
189
174
  Collapse = new() { "description" }
190
175
  }
191
176
  },
192
- Image = new()
177
+ Image = new List<ImageDataRequest>
193
178
  {
194
- { "header_logo", new() { Src = "https://example.com/logo.png", Width = 200, Fit = "contain" } }
179
+ new ImageDataRequest { Key = "header_logo", Src = "https://example.com/logo.png", Width = 200, Fit = ImageDataRequestFit.Contain }
195
180
  },
196
- QrCode = new()
181
+ Qrcode = new List<QrCodeDataRequest>
197
182
  {
198
- { "payment_qr", new() { Text = "https://payment.link/123456", Size = 150 } }
183
+ new QrCodeDataRequest { Key = "payment_qr", Text = "https://payment.link/123456", Size = 150 }
199
184
  },
200
- Barcode = new()
185
+ Barcode = new List<BarcodeDataRequest>
201
186
  {
202
- { "product_barcode", new() { Text = "PROD-123456789", Format = "Code128", Width = 300, Height = 80 } }
187
+ new BarcodeDataRequest { Key = "product_barcode", Text = "PROD-123456789", Format = "Code128", Width = 300, Height = 80 }
203
188
  }
204
189
  };
205
190
 
@@ -219,24 +204,28 @@ Method: `render.postRenderExcelTemplate` (Node) / `PostRenderExcelTemplateAsync`
219
204
  const response = await qorstackApi.render.postRenderExcelTemplate({
220
205
  templateKey: "XLSX-1234-5678",
221
206
  fileName: "Sales_Report_Q1",
222
- replace: {
223
- report_period: "Q1 2026",
224
- generated_by: "Sales Team",
225
- },
207
+ replace: [
208
+ { key: "report_period", value: "Q1 2026" },
209
+ { key: "generated_by", value: "Sales Team" },
210
+ ],
226
211
  table: [
227
212
  {
213
+ columns: ['region', 'product', 'qty', 'total'],
228
214
  rows: [
229
- { region: "North", product: "A", qty: 10, total: 1000 },
230
- { region: "North", product: "B", qty: 5, total: 500 },
231
- { region: "South", product: "A", qty: 8, total: 800 },
215
+ ['North', 'A', 10, 1000],
216
+ ['North', 'B', 5, 500],
217
+ ['South', 'A', 8, 800]
232
218
  ],
233
219
  autoFilter: true,
234
220
  freezeHeader: true,
235
221
  autoFitColumns: true,
236
222
  asExcelTable: true,
237
223
  excelTableStyle: "TableStyleMedium2",
238
- generateTotals: { total: "sum", qty: "sum" },
239
- numberFormat: { total: "#,##0.00" },
224
+ generateTotals: [
225
+ { key: "total", value: "SUM" },
226
+ { key: "qty", value: "SUM" },
227
+ ],
228
+ numberFormat: [{ key: "total", value: "#,##0.00" }],
240
229
  },
241
230
  ],
242
231
  });
@@ -250,28 +239,33 @@ var request = new ExcelFromTemplateRequest
250
239
  {
251
240
  TemplateKey = "XLSX-1234-5678",
252
241
  FileName = "Sales_Report_Q1",
253
- Replace = new()
242
+ Replace = new List<KeyValueRequest>
254
243
  {
255
- { "report_period", "Q1 2026" },
256
- { "generated_by", "Sales Team" }
244
+ new KeyValueRequest { Key = "report_period", Value = "Q1 2026" },
245
+ new KeyValueRequest { Key = "generated_by", Value = "Sales Team" }
257
246
  },
258
- Table = new()
247
+ Table = new List<ExcelTableDataRequest>
259
248
  {
260
- new()
249
+ new ExcelTableDataRequest
261
250
  {
262
- Rows = new()
251
+ Columns = new List<string> { "region", "product", "qty", "total" },
252
+ Rows = new List<ICollection<object>>
263
253
  {
264
- new Dictionary<string, object> { { "region", "North" }, { "product", "A" }, { "qty", 10 }, { "total", 1000 } },
265
- new Dictionary<string, object> { { "region", "North" }, { "product", "B" }, { "qty", 5 }, { "total", 500 } },
266
- new Dictionary<string, object> { { "region", "South" }, { "product", "A" }, { "qty", 8 }, { "total", 800 } }
254
+ new List<object> { "North", "A", 10, 1000 },
255
+ new List<object> { "North", "B", 5, 500 },
256
+ new List<object> { "South", "A", 8, 800 }
267
257
  },
268
258
  AutoFilter = true,
269
259
  FreezeHeader = true,
270
260
  AutoFitColumns = true,
271
261
  AsExcelTable = true,
272
262
  ExcelTableStyle = "TableStyleMedium2",
273
- GenerateTotals = new() { { "total", "sum" }, { "qty", "sum" } },
274
- NumberFormat = new() { { "total", "#,##0.00" } }
263
+ GenerateTotals = new List<KeyValueRequest>
264
+ {
265
+ new KeyValueRequest { Key = "total", Value = "SUM" },
266
+ new KeyValueRequest { Key = "qty", Value = "SUM" }
267
+ },
268
+ NumberFormat = new List<KeyValueRequest> { new KeyValueRequest { Key = "total", Value = "#,##0.00" } }
275
269
  }
276
270
  }
277
271
  };
@@ -332,33 +326,72 @@ const response = await qorstackApi.render.postRenderWordTemplateSandbox(
332
326
 
333
327
  ---
334
328
 
335
- ### 6. Inspect render jobs
329
+ ### 6. Async renders *(Pro)* and job status
330
+
331
+ With `"async": true` the API queues the render and answers `202` with a job id instead of the file.
332
+ Use the helpers below — they handle both answers, poll the job, and turn `429` (too many pending
333
+ jobs) into a typed error. Without a Pro license the API answers `403`.
336
334
 
337
335
  ```typescript
338
- // List jobs
339
- const jobs = await qorstackApi.render.getRenderJobs({
340
- page: 1,
341
- pageSize: 20,
342
- // templateKey: "...", status: "Success" | "Failed" | "Pending"
343
- });
336
+ import { QorstackApi, RenderQueueFullError } from "qorstack-report-sdk";
337
+
338
+ const outcome = await qorstackApi.renderWord({ templateKey: "invoice", async: true, replace: [/* … */] });
339
+
340
+ if (outcome.kind === "queued") {
341
+ const job = await qorstackApi.waitForJob(outcome.job.jobId!, { intervalMs: 2000, timeoutMs: 600_000 });
342
+ console.log(job.downloadUrl); // fresh link once status is "success"
343
+ } else {
344
+ console.log(outcome.download.downloadUrl); // rendered in this request (async not set)
345
+ }
344
346
 
345
- // Get one
346
- const job = await qorstackApi.render.getRenderJobById("job-id-here");
347
+ // const job = await qorstackApi.getRenderJob(jobId); // single status check
348
+ // catch (e) { if (e instanceof RenderQueueFullError) retry after e.retryAfterSeconds }
347
349
  ```
348
350
 
351
+ ```csharp
352
+ var outcome = await qorstackApi.RenderWordAsync(new PdfFromTemplateRequest { TemplateKey = "invoice", Async = true });
353
+
354
+ var link = outcome.IsQueued
355
+ ? (await qorstackApi.WaitForJobAsync(outcome.Job!.JobId, TimeSpan.FromSeconds(2), TimeSpan.FromMinutes(10))).DownloadUrl
356
+ : outcome.Download!.DownloadUrl;
357
+
358
+ // RenderQueueFullException.RetryAfter on 429 · RenderJobFailedException when the job fails
359
+ // var job = await qorstackApi.GetRenderJobAsync(jobId);
360
+ ```
361
+
362
+ `renderWord` / `renderExcel` (`RenderWordAsync` / `RenderExcelAsync`) also work without `async` —
363
+ they then always return the download. The generated `render.postRender…` methods stay available.
364
+
349
365
  ---
350
366
 
351
367
  ## Data Binding Reference
352
368
 
369
+ `replace`, `image`, `qrcode` and `barcode` are **keyed arrays**: every item names the
370
+ template marker it fills via `key` (SDK types: `KeyValueRequest`, `ImageDataRequest`,
371
+ `QrCodeDataRequest`, `BarcodeDataRequest`). Tables list their column names once in
372
+ `columns`; each row in `rows` holds the values in that order.
373
+
374
+ The API validates the whole request first and returns every problem in one `400`,
375
+ keyed by JSON path (`replace[3].key`, `table[0].rows[12]`, `image[0].fit`).
376
+
377
+ Set `templateVersion` to render an older stored version of the template (omit it for the
378
+ current version; an unknown version returns `404`).
379
+
380
+ > **SDK 1.0 / API v2.0:** the v1 forms were removed and now return `400`: object maps
381
+ > (`"replace": { "customerName": "..." }`), rows as objects or `{ key, value }` cells,
382
+ > single-string `sort` / `verticalMerge` / `collapse`, and `fileType` on the Excel endpoint.
383
+ > Upgrading from SDK 0.x? See
384
+ > [Migrating from v1](../backend/documents/10_RENDER-API-SUMMARY.md#migrating-from-v1).
385
+
353
386
  ### Text replacement (`replace`)
354
387
 
355
- Replaces `{{variable}}` placeholders with strings.
388
+ Replaces `{{variable}}` placeholders with strings (`KeyValueRequest[]`).
356
389
 
357
390
  ```json
358
- "replace": {
359
- "customerName": "Acme Corp",
360
- "totalAmount": "$1,050.00"
361
- }
391
+ "replace": [
392
+ { "key": "customerName", "value": "Acme Corp" },
393
+ { "key": "totalAmount", "value": "$1,050.00" }
394
+ ]
362
395
  ```
363
396
 
364
397
  ### Word table (`table` — `WordTableDataRequest[]`)
@@ -366,9 +399,10 @@ Replaces `{{variable}}` placeholders with strings.
366
399
  ```json
367
400
  "table": [
368
401
  {
402
+ "columns": ["category", "description", "amount"],
369
403
  "rows": [
370
- { "name": "Value A", "last_name": "Value B" },
371
- { "name": "Value C", "last_name": "Value D" }
404
+ ["Hardware", "Laptop", 1200],
405
+ ["Hardware", "Monitor", 300]
372
406
  ],
373
407
  "verticalMerge": ["category"],
374
408
  "collapse": ["description"]
@@ -378,7 +412,9 @@ Replaces `{{variable}}` placeholders with strings.
378
412
 
379
413
  | Field | Description |
380
414
  | --- | --- |
381
- | `rows` | Array of row objects. Keys match column placeholders. |
415
+ | `columns` | Column names, listed once — each matches a `{{row:<name>}}` placeholder. Unique; required when there are rows. |
416
+ | `rows` | Array of rows; each row is an array of values in `columns` order (string, number, boolean or null). A row with the wrong number of values returns `400` naming the row. |
417
+ | `sort` | `[{ "field": column, "direction": "asc" \| "desc" }]`. |
382
418
  | `verticalMerge` | Columns whose adjacent identical values are merged vertically. |
383
419
  | `collapse` | Columns whose empty/duplicate values collapse to keep layout tidy. |
384
420
 
@@ -387,14 +423,17 @@ Replaces `{{variable}}` placeholders with strings.
387
423
  ```json
388
424
  "table": [
389
425
  {
390
- "rows": [{ "region": "North", "qty": 10 }],
426
+ "columns": ["region", "qty"],
427
+ "rows": [
428
+ ["North", 10]
429
+ ],
391
430
  "autoFilter": true,
392
431
  "freezeHeader": true,
393
432
  "autoFitColumns": true,
394
433
  "asExcelTable": true,
395
434
  "excelTableStyle": "TableStyleMedium2",
396
- "generateTotals": { "qty": "sum" },
397
- "numberFormat": { "qty": "#,##0" }
435
+ "generateTotals": [ { "key": "qty", "value": "SUM" } ],
436
+ "numberFormat": [ { "key": "qty", "value": "#,##0" } ]
398
437
  }
399
438
  ]
400
439
  ```
@@ -405,8 +444,8 @@ Replaces `{{variable}}` placeholders with strings.
405
444
  | `freezeHeader` | Freeze the header row. |
406
445
  | `autoFitColumns` | Auto-fit column widths to content. |
407
446
  | `asExcelTable` / `excelTableStyle` | Render as native Excel table with style. |
408
- | `generateTotals` | Per-column aggregation (`sum`, `avg`, `count`, `min`, `max`). |
409
- | `numberFormat` | Excel number format string per column. |
447
+ | `generateTotals` | Per-column aggregation: `[{ "key": column, "value": "SUM" }]` (`SUM`, `AVERAGE`, `COUNT`, `MIN`, `MAX`). |
448
+ | `numberFormat` | Excel number format per column: `[{ "key": column, "value": "#,##0.00" }]`. |
410
449
  | `conditionalFormat` | Conditional formatting rules. |
411
450
  | `splitToSheets` | Split rows into multiple sheets by a column value. |
412
451
  | `sort` | Sort rows before render. |
@@ -416,14 +455,15 @@ Replaces `{{variable}}` placeholders with strings.
416
455
  Replaces `{{image:logo}}`. Accepts URL or Base64.
417
456
 
418
457
  ```json
419
- "image": {
420
- "logo": {
458
+ "image": [
459
+ {
460
+ "key": "logo",
421
461
  "src": "https://example.com/logo.png",
422
462
  "width": 150,
423
463
  "height": 50,
424
464
  "fit": "contain"
425
465
  }
426
- }
466
+ ]
427
467
  ```
428
468
 
429
469
  ### QR codes (`qrcode`)
@@ -431,13 +471,14 @@ Replaces `{{image:logo}}`. Accepts URL or Base64.
431
471
  Replaces `{{qr:website}}`.
432
472
 
433
473
  ```json
434
- "qrcode": {
435
- "website": {
474
+ "qrcode": [
475
+ {
476
+ "key": "website",
436
477
  "text": "https://qorstack.dev",
437
478
  "size": 200,
438
479
  "color": "#000000"
439
480
  }
440
- }
481
+ ]
441
482
  ```
442
483
 
443
484
  ### Barcodes (`barcode`)
@@ -445,15 +486,16 @@ Replaces `{{qr:website}}`.
445
486
  Replaces `{{barcode:productId}}`. Default format: `Code128`.
446
487
 
447
488
  ```json
448
- "barcode": {
449
- "productId": {
489
+ "barcode": [
490
+ {
491
+ "key": "productId",
450
492
  "text": "1234567890",
451
493
  "format": "Code128",
452
494
  "includeText": true,
453
495
  "width": 300,
454
496
  "height": 80
455
497
  }
456
- }
498
+ ]
457
499
  ```
458
500
 
459
501
  ### Output options
@@ -461,6 +503,7 @@ Replaces `{{barcode:productId}}`. Default format: `Code128`.
461
503
  | Field | Type | Description |
462
504
  | --- | --- | --- |
463
505
  | `fileName` | string | Output file name (without extension). |
506
+ | `templateVersion` | integer | Render this stored version of the template; omit for the current version (unknown → `404`). |
464
507
  | `fileType` | string | Word only: `"pdf"` (default) or `"docx"`. |
465
508
  | `zipOutput` | boolean | If `true`, bundle the output into a `.zip` archive. |
466
509
  | `pdfPassword` | object | *(Pro)* Encrypt the PDF and set permissions. |
@@ -370,41 +370,68 @@ export interface RenderResponse {
370
370
  }
371
371
  export type PdfFromTemplateRequest = DocumentProcessingRequestBase & {
372
372
  templateKey?: string;
373
+ /**
374
+ * Render this stored version of the template. Omit for the current version.
375
+ * @format int32
376
+ * @min 1
377
+ */
378
+ templateVersion?: number | null;
373
379
  fileName?: string | null;
374
- fileType?: string;
380
+ /** Output format: pdf (default) or docx */
381
+ fileType?: "pdf" | "docx";
375
382
  table?: WordTableDataRequest[] | null;
376
383
  };
377
384
  export type WordTableDataRequest = TableDataRequest & {
378
385
  repeatHeader?: boolean;
379
386
  };
380
387
  export interface TableDataRequest {
381
- rows?: Record<string, any>[];
388
+ /** Column marker names ({{row:name}}), listed once, in the order of the row values. Unique. */
389
+ columns: string[];
390
+ /** Rows; each row is an array of cell values in the order of columns, e.g. [["A", 2]] */
391
+ rows: (string | number | boolean | null)[][];
382
392
  sort?: SortDefinition[] | null;
383
393
  verticalMerge?: string[] | null;
384
394
  collapse?: string[] | null;
385
395
  }
386
396
  export interface SortDefinition {
387
397
  field?: string;
388
- direction?: string;
398
+ direction?: "asc" | "desc";
389
399
  }
390
400
  export interface DocumentProcessingRequestBase {
391
- replace?: Record<string, string>;
392
- image?: Record<string, ImageDataRequest>;
393
- qrcode?: Record<string, QrCodeDataRequest>;
394
- barcode?: Record<string, BarcodeDataRequest>;
401
+ /** Variable replacements ({{variable}}): [{ "key": "customer_name", "value": "ACME" }] */
402
+ replace?: KeyValueRequest[] | null;
403
+ /** Images for {{image:key}} markers */
404
+ image?: ImageDataRequest[] | null;
405
+ /** QR codes for {{qr:key}} markers */
406
+ qrcode?: QrCodeDataRequest[] | null;
407
+ /** Barcodes for {{barcode:key}} markers */
408
+ barcode?: BarcodeDataRequest[] | null;
395
409
  pdfPassword?: PdfPasswordRequest | null;
396
410
  watermark?: PdfWatermarkRequest | null;
397
411
  zipOutput?: boolean;
412
+ /** Pro: queue the render and return 202 with a job id instead of the file. Poll GET /render/jobs/{jobId} or set webhookUrl. */
413
+ async?: boolean;
414
+ /**
415
+ * With async: true, receives a POST {jobId, status, downloadUrl?, expiresIn?, error?} when the job finishes. Signed with X-Qorstack-Signature when the server has a signing secret.
416
+ * @format uri
417
+ * @maxLength 2048
418
+ */
419
+ webhookUrl?: string | null;
398
420
  }
399
421
  export interface ImageDataRequest {
422
+ /** Image marker name ({{image:key}}) */
423
+ key?: string;
400
424
  src?: string;
401
425
  /** @format int32 */
402
426
  width?: number | null;
403
427
  /** @format int32 */
404
428
  height?: number | null;
405
- fit?: string | null;
429
+ /** How the image fills its box: cover, contain (default) or fill */
430
+ fit?: "cover" | "contain" | "fill" | null;
406
431
  }
407
432
  export interface QrCodeDataRequest {
433
+ /** QR marker name ({{qr:key}}) */
434
+ key?: string;
408
435
  text?: string;
409
436
  /** @format int32 */
410
437
  size?: number;
@@ -413,6 +440,8 @@ export interface QrCodeDataRequest {
413
440
  logo?: string | null;
414
441
  }
415
442
  export interface BarcodeDataRequest {
443
+ /** Barcode marker name ({{barcode:key}}) */
444
+ key?: string;
416
445
  text?: string;
417
446
  format?: string;
418
447
  /** @format int32 */
@@ -455,7 +484,7 @@ export interface KeyValuePairOfStringAndJsonNode {
455
484
  value?: JsonNode | null;
456
485
  }
457
486
  export interface JsonNode {
458
- underlyingElement?: any;
487
+ underlyingElement?: any | null;
459
488
  options?: JsonNodeOptions | null;
460
489
  parent?: JsonNode | null;
461
490
  root?: JsonNode;
@@ -475,8 +504,13 @@ export interface ExportExcelUrlResponse {
475
504
  }
476
505
  export type ExcelFromTemplateRequest = DocumentProcessingRequestBase & {
477
506
  templateKey?: string;
507
+ /**
508
+ * Render this stored version of the template. Omit for the current version.
509
+ * @format int32
510
+ * @min 1
511
+ */
512
+ templateVersion?: number | null;
478
513
  fileName?: string | null;
479
- fileType?: string;
480
514
  table?: ExcelTableDataRequest[] | null;
481
515
  };
482
516
  export type ExcelTableDataRequest = TableDataRequest & {
@@ -486,8 +520,10 @@ export type ExcelTableDataRequest = TableDataRequest & {
486
520
  asExcelTable?: boolean;
487
521
  excelTableStyle?: string | null;
488
522
  outline?: boolean;
489
- generateTotals?: Record<string, string>;
490
- numberFormat?: Record<string, string>;
523
+ /** Totals per column: [{ "key": "amount", "value": "sum" }] */
524
+ generateTotals?: KeyValueRequest[] | null;
525
+ /** Number format per column: [{ "key": "amount", "value": "#,##0.00" }] */
526
+ numberFormat?: KeyValueRequest[] | null;
491
527
  conditionalFormat?: ConditionalFormatConfigRequest[] | null;
492
528
  splitToSheets?: SplitToSheetsConfigRequest | null;
493
529
  };
@@ -496,7 +532,7 @@ export interface ConditionalFormatConfigRequest {
496
532
  rules?: ConditionalFormatRuleRequest[];
497
533
  }
498
534
  export interface ConditionalFormatRuleRequest {
499
- value?: any;
535
+ value?: any | null;
500
536
  operator?: string | null;
501
537
  fontColor?: string | null;
502
538
  backgroundColor?: string | null;
@@ -751,6 +787,21 @@ export interface ApiKeyDto {
751
787
  /** @format date-time */
752
788
  updatedDatetime?: string | null;
753
789
  }
790
+ /** Marker name with a text value (replace / generateTotals / numberFormat) */
791
+ export interface KeyValueRequest {
792
+ /** Marker / column name */
793
+ key?: string;
794
+ /** Text value */
795
+ value?: string;
796
+ }
797
+ export interface ExportPdfAsyncResponse {
798
+ /** @format uuid */
799
+ jobId?: string;
800
+ /** pending → processing → success | failed */
801
+ status?: string;
802
+ /** GET this URL to poll the job; it returns a fresh downloadUrl once status is success. */
803
+ statusUrl?: string | null;
804
+ }
754
805
  export type QueryParamsType = Record<string | number, any>;
755
806
  export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
756
807
  export interface FullRequestParams extends Omit<RequestInit, "body"> {
@@ -0,0 +1,41 @@
1
+ import type { ExportPdfAsyncResponse, HttpResponse } from "./QorstackApi";
2
+ /** Result of a render call: the finished file, or a queued job (async: true, Pro). */
3
+ export type RenderOutcome<TDownload> = {
4
+ kind: "download";
5
+ download: TDownload;
6
+ } | {
7
+ kind: "queued";
8
+ job: ExportPdfAsyncResponse;
9
+ };
10
+ /** Status of an async render job (GET /render/jobs/{id}). */
11
+ export interface RenderJobStatus {
12
+ id: string;
13
+ /** pending → processing → success | failed */
14
+ status: "pending" | "processing" | "success" | "failed" | string;
15
+ /** Fresh download link once status is "success". */
16
+ downloadUrl?: string | null;
17
+ downloadUrlExpiresAt?: string | null;
18
+ errorMessage?: string | null;
19
+ durationMs?: number | null;
20
+ fileSizeBytes?: number | null;
21
+ }
22
+ /** HTTP 429: too many unfinished async renders for this user. */
23
+ export declare class RenderQueueFullError extends Error {
24
+ readonly retryAfterSeconds: number | null;
25
+ constructor(retryAfterSeconds: number | null);
26
+ }
27
+ /** The async render job finished with status "failed". */
28
+ export declare class RenderJobFailedError extends Error {
29
+ readonly job: RenderJobStatus;
30
+ constructor(job: RenderJobStatus);
31
+ }
32
+ export interface WaitForJobOptions {
33
+ /** Poll interval in ms (default 2000). */
34
+ intervalMs?: number;
35
+ /** Give up after this many ms (default 600000 = 10 min). */
36
+ timeoutMs?: number;
37
+ }
38
+ /** Turns a raw render response into a RenderOutcome; maps 429 to RenderQueueFullError. */
39
+ export declare function toRenderOutcome<T>(call: Promise<HttpResponse<T, unknown>>): Promise<RenderOutcome<T>>;
40
+ /** Polls until the job finishes; resolves with the job (downloadUrl set) or rejects on failure / timeout. */
41
+ export declare function waitForJob(getJob: (jobId: string) => Promise<RenderJobStatus>, jobId: string, { intervalMs, timeoutMs }?: WaitForJobOptions): Promise<RenderJobStatus>;
package/dist/async.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RenderJobFailedError = exports.RenderQueueFullError = void 0;
4
+ exports.toRenderOutcome = toRenderOutcome;
5
+ exports.waitForJob = waitForJob;
6
+ /** HTTP 429: too many unfinished async renders for this user. */
7
+ class RenderQueueFullError extends Error {
8
+ constructor(retryAfterSeconds) {
9
+ super("Too many async renders are pending. Retry later.");
10
+ this.retryAfterSeconds = retryAfterSeconds;
11
+ this.name = "RenderQueueFullError";
12
+ }
13
+ }
14
+ exports.RenderQueueFullError = RenderQueueFullError;
15
+ /** The async render job finished with status "failed". */
16
+ class RenderJobFailedError extends Error {
17
+ constructor(job) {
18
+ var _a;
19
+ super(`Render job ${job.id} failed: ${(_a = job.errorMessage) !== null && _a !== void 0 ? _a : "unknown error"}`);
20
+ this.job = job;
21
+ this.name = "RenderJobFailedError";
22
+ }
23
+ }
24
+ exports.RenderJobFailedError = RenderJobFailedError;
25
+ /** Turns a raw render response into a RenderOutcome; maps 429 to RenderQueueFullError. */
26
+ async function toRenderOutcome(call) {
27
+ var _a, _b;
28
+ try {
29
+ const res = await call;
30
+ if (res.status === 202) {
31
+ return { kind: "queued", job: res.data };
32
+ }
33
+ return { kind: "download", download: res.data };
34
+ }
35
+ catch (err) {
36
+ const res = err;
37
+ if (res && res.status === 429) {
38
+ const header = (_b = (_a = res.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, "Retry-After");
39
+ const seconds = header ? Number.parseInt(header, 10) : NaN;
40
+ throw new RenderQueueFullError(Number.isFinite(seconds) ? seconds : null);
41
+ }
42
+ throw err;
43
+ }
44
+ }
45
+ /** Polls until the job finishes; resolves with the job (downloadUrl set) or rejects on failure / timeout. */
46
+ async function waitForJob(getJob, jobId, { intervalMs = 2000, timeoutMs = 600000 } = {}) {
47
+ const deadline = Date.now() + timeoutMs;
48
+ for (;;) {
49
+ const job = await getJob(jobId);
50
+ if (job.status === "success")
51
+ return job;
52
+ if (job.status === "failed")
53
+ throw new RenderJobFailedError(job);
54
+ if (Date.now() + intervalMs > deadline) {
55
+ throw new Error(`Render job ${jobId} did not finish in time (last status: ${job.status}).`);
56
+ }
57
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
58
+ }
59
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { Api } from "./QorstackApi";
1
+ import { Api, ExcelFromTemplateRequest, ExportExcelUrlResponse, ExportPdfUrlResponse, PdfFromTemplateRequest, RequestParams } from "./QorstackApi";
2
+ import { RenderJobStatus, RenderOutcome, WaitForJobOptions } from "./async";
2
3
  export * from "./QorstackApi";
4
+ export * from "./async";
3
5
  export declare class QorstackApi<SecurityDataType = unknown> extends Api<SecurityDataType> {
4
6
  /**
5
7
  * Creates a new instance of QorstackApi.
@@ -9,4 +11,12 @@ export declare class QorstackApi<SecurityDataType = unknown> extends Api<Securit
9
11
  baseUrl?: string;
10
12
  securityData?: SecurityDataType;
11
13
  });
14
+ /** Render from a Word template: the file link, or (with async: true, Pro) the queued job. */
15
+ renderWord(data: PdfFromTemplateRequest, params?: RequestParams): Promise<RenderOutcome<ExportPdfUrlResponse>>;
16
+ /** Render from an Excel template: the file link, or (with async: true, Pro) the queued job. */
17
+ renderExcel(data: ExcelFromTemplateRequest, params?: RequestParams): Promise<RenderOutcome<ExportExcelUrlResponse>>;
18
+ /** GET /render/jobs/{jobId}. */
19
+ getRenderJob(jobId: string, params?: RequestParams): Promise<RenderJobStatus>;
20
+ /** Polls an async job until it finishes; resolves with downloadUrl set, rejects on failure or timeout. */
21
+ waitForJob(jobId: string, options?: WaitForJobOptions): Promise<RenderJobStatus>;
12
22
  }
package/dist/index.js CHANGED
@@ -16,7 +16,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.QorstackApi = void 0;
18
18
  const QorstackApi_1 = require("./QorstackApi");
19
+ const async_1 = require("./async");
19
20
  __exportStar(require("./QorstackApi"), exports);
21
+ __exportStar(require("./async"), exports);
20
22
  class QorstackApi extends QorstackApi_1.Api {
21
23
  /**
22
24
  * Creates a new instance of QorstackApi.
@@ -36,8 +38,11 @@ class QorstackApi extends QorstackApi_1.Api {
36
38
  // Ignore errors accessing process.env
37
39
  }
38
40
  }
39
- // Fallback to default if still not found
40
- baseUrl = baseUrl || "https://api.qorstack.dev";
41
+ // Fallback to the self-host default if still not found. Since v2 the API lives under /api on the
42
+ // same port as the web UI; a v1-style URL without it (http://host:8080) gets /api appended.
43
+ baseUrl = (baseUrl || "http://localhost:8080/api").replace(/\/+$/, "");
44
+ if (!/\/api$/.test(baseUrl))
45
+ baseUrl = `${baseUrl}/api`;
41
46
  super({
42
47
  baseUrl,
43
48
  securityWorker: (securityData) => {
@@ -51,5 +56,28 @@ class QorstackApi extends QorstackApi_1.Api {
51
56
  this.setSecurityData(config.securityData);
52
57
  }
53
58
  }
59
+ /** Render from a Word template: the file link, or (with async: true, Pro) the queued job. */
60
+ renderWord(data, params = {}) {
61
+ return (0, async_1.toRenderOutcome)(this.render.postRenderWordTemplate(data, params));
62
+ }
63
+ /** Render from an Excel template: the file link, or (with async: true, Pro) the queued job. */
64
+ renderExcel(data, params = {}) {
65
+ return (0, async_1.toRenderOutcome)(this.render.postRenderExcelTemplate(data, params));
66
+ }
67
+ /** GET /render/jobs/{jobId}. */
68
+ async getRenderJob(jobId, params = {}) {
69
+ const res = await this.request({
70
+ path: `/render/jobs/${encodeURIComponent(jobId)}`,
71
+ method: "GET",
72
+ secure: true,
73
+ format: "json",
74
+ ...params,
75
+ });
76
+ return res.data;
77
+ }
78
+ /** Polls an async job until it finishes; resolves with downloadUrl set, rejects on failure or timeout. */
79
+ waitForJob(jobId, options) {
80
+ return (0, async_1.waitForJob)((id) => this.getRenderJob(id), jobId, options);
81
+ }
54
82
  }
55
83
  exports.QorstackApi = QorstackApi;
package/package.json CHANGED
@@ -1,38 +1,45 @@
1
- {
2
- "name": "qorstack-report-sdk",
3
- "version": "0.1.1",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "files": [
7
- "dist"
8
- ],
9
- "scripts": {
10
- "sync-config": "node scripts/sync-config.js",
11
- "prebuild": "yarn sync-config",
12
- "build": "tsc",
13
- "test": "echo \"Error: no test specified\" && exit 1",
14
- "gen:nodejs": "swagger-typescript-api generate -p ../backend/swagger-sdk.json -o ./src/nodejs --name QorstackApi.ts",
15
- "gen:dotnet": "nswag openapi2csclient /input:../backend/swagger-sdk.json /output:src/dotnet/QorstackApi.cs /namespace:Qorstack.Report.Sdk /ClassName:QorstackApi",
16
- "gen:all": "yarn gen:nodejs && yarn gen:dotnet"
17
- },
18
- "keywords": [
19
- "qorstack",
20
- "report",
21
- "sdk",
22
- "pdf",
23
- "word"
24
- ],
25
- "author": "Qorstack",
26
- "license": "MIT",
27
- "description": "Qorstack Report SDK for Node.js and .NET",
28
- "repository": {
29
- "type": "git",
30
- "url": "https://github.com/qorstack/qorstack-report"
31
- },
32
- "devDependencies": {
33
- "@types/node": "^25.2.1",
34
- "nswag": "^14.6.3",
35
- "swagger-typescript-api": "^13.2.16",
36
- "typescript": "^5.9.3"
37
- }
38
- }
1
+ {
2
+ "name": "qorstack-report-sdk",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "sync-config": "node scripts/sync-config.js",
11
+ "prebuild": "pnpm sync-config",
12
+ "build": "tsc",
13
+ "test": "echo \"Error: no test specified\" && exit 1",
14
+ "gen:nodejs": "swagger-typescript-api generate -p ../backend/swagger-sdk.json -o ./src/nodejs --name QorstackApi.ts",
15
+ "gen:dotnet": "nswag openapi2csclient /input:../backend/swagger-sdk.json /output:src/dotnet/QorstackApi.cs /namespace:Qorstack.Report.Sdk /ClassName:QorstackApi",
16
+ "gen:all": "pnpm gen:nodejs && pnpm gen:dotnet"
17
+ },
18
+ "keywords": [
19
+ "qorstack",
20
+ "report",
21
+ "sdk",
22
+ "pdf",
23
+ "word"
24
+ ],
25
+ "author": "Qorstack",
26
+ "license": "MIT",
27
+ "description": "Qorstack Report SDK for Node.js and .NET",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/qorstack/qorstack-report"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.2.1",
34
+ "nswag": "^14.6.3",
35
+ "swagger-typescript-api": "13.12.2",
36
+ "typescript": "^5.9.3"
37
+ },
38
+ "packageManager": "pnpm@9.15.9",
39
+ "pnpm": {
40
+ "overrides": {
41
+ "axios": "1.20.0",
42
+ "defu": "6.1.5"
43
+ }
44
+ }
45
+ }