n8n-nodes-pdfkraft 0.2.0 → 0.4.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
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# n8n-nodes-pdfkraft
|
|
2
|
+
|
|
3
|
+
An n8n community node for [PDFKraft](https://pdfkraft.com) — generate PDFs and images from templates using your n8n workflows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
In your n8n instance, go to **Settings → Community Nodes → Install** and enter:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
n8n-nodes-pdfkraft
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Credentials
|
|
14
|
+
|
|
15
|
+
After installation, create a **PDFKraft API** credential:
|
|
16
|
+
|
|
17
|
+
1. Go to [pdfkraft.com/dashboard/account](https://pdfkraft.com/dashboard/account) and copy your API key.
|
|
18
|
+
2. In n8n, go to **Credentials → New → PDFKraft API**.
|
|
19
|
+
3. Paste your API key. Leave the Base URL as `https://api.pdfkraft.com/api/v1`.
|
|
20
|
+
|
|
21
|
+
## PDFKraft Node
|
|
22
|
+
|
|
23
|
+
Provides six operations:
|
|
24
|
+
|
|
25
|
+
### Generate Document
|
|
26
|
+
Queues a document for generation and returns immediately with a document ID. Use this when you want to fire-and-forget and handle completion via a webhook trigger.
|
|
27
|
+
|
|
28
|
+
**Inputs:** Template, Payload (JSON), Filename (optional)
|
|
29
|
+
**Output:** `{ id, status: "pending", ... }`
|
|
30
|
+
|
|
31
|
+
### Generate Document and Wait
|
|
32
|
+
Generates a document and polls until complete. Returns the final document record including `download_url`.
|
|
33
|
+
|
|
34
|
+
**Inputs:** Template, Payload (JSON), Filename, Timeout (seconds)
|
|
35
|
+
**Output:** `{ id, status: "success", download_url, filename, ... }`
|
|
36
|
+
|
|
37
|
+
### Generate Document and Download
|
|
38
|
+
The most common operation. Generates a document, waits for completion, and downloads the file — all in one step.
|
|
39
|
+
|
|
40
|
+
**Inputs:** Template, Payload (JSON), Filename, Timeout, Output Field Name
|
|
41
|
+
**Output:** JSON (`id`, `filename`, `download_url`) + Binary file in the configured output field
|
|
42
|
+
|
|
43
|
+
### Get Document
|
|
44
|
+
Fetches the current status and metadata of any document by ID.
|
|
45
|
+
|
|
46
|
+
**Inputs:** Document ID
|
|
47
|
+
**Output:** Full document record
|
|
48
|
+
|
|
49
|
+
### Download Document
|
|
50
|
+
Downloads a completed document as binary data.
|
|
51
|
+
|
|
52
|
+
**Inputs:** Document ID, Output Field Name
|
|
53
|
+
**Output:** JSON (`documentId`, `filename`, `download_url`) + Binary file
|
|
54
|
+
|
|
55
|
+
### List Documents
|
|
56
|
+
Returns generated documents with optional status filtering and pagination.
|
|
57
|
+
|
|
58
|
+
**Inputs:** Status Filter (All / Success / Pending / Failure), Limit or Return All
|
|
59
|
+
**Output:** One item per document
|
|
60
|
+
|
|
61
|
+
### List Templates
|
|
62
|
+
Returns all templates in your account.
|
|
63
|
+
|
|
64
|
+
**Output:** One item per template
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Template Variables helper
|
|
69
|
+
|
|
70
|
+
When you select a template, open the **Template Variables** dropdown to see what variables that template expects, along with their sample values. This is a read-only reference — it does not affect the generated document.
|
|
71
|
+
|
|
72
|
+
To define sample data for your templates, open the template editor in PDFKraft and fill in the **Sample Data** section.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## PDFKraft Trigger Node
|
|
77
|
+
|
|
78
|
+
Listens for webhook events from PDFKraft and triggers your workflow when documents are generated.
|
|
79
|
+
|
|
80
|
+
### Setup
|
|
81
|
+
|
|
82
|
+
1. In PDFKraft, go to **Settings → Webhooks → Create Endpoint**.
|
|
83
|
+
2. Set the URL to the webhook URL shown in your n8n trigger node.
|
|
84
|
+
3. Copy the **Secret** shown after creation.
|
|
85
|
+
4. In n8n, paste the secret into the **Webhook Secret** field.
|
|
86
|
+
|
|
87
|
+
### Options
|
|
88
|
+
|
|
89
|
+
| Field | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Webhook Secret | The secret from your PDFKraft webhook endpoint. Used to verify request authenticity. |
|
|
92
|
+
| Verify Signature | Recommended. Rejects requests that don't have a valid HMAC signature. |
|
|
93
|
+
| Event Types | Choose Document Succeeded, Document Failed, or both. |
|
|
94
|
+
| Template Filter | Only fire for documents from a specific template. Select "All Templates" to receive all events. |
|
|
95
|
+
|
|
96
|
+
### Output
|
|
97
|
+
|
|
98
|
+
The trigger outputs the full webhook payload:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"event": "document.success",
|
|
103
|
+
"document": {
|
|
104
|
+
"id": "...",
|
|
105
|
+
"status": "success",
|
|
106
|
+
"filename": "invoice-1234.pdf",
|
|
107
|
+
"download_url": "https://...",
|
|
108
|
+
"document_template_identifier": "invoice",
|
|
109
|
+
"meta": {},
|
|
110
|
+
"generated_at": "2026-06-22T10:00:00.000Z"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Example Workflow
|
|
116
|
+
|
|
117
|
+
**Generate an invoice PDF and save it to Google Drive:**
|
|
118
|
+
|
|
119
|
+
1. **HTTP Request** (or any trigger) → provides order data
|
|
120
|
+
2. **PDFKraft** → Generate Document and Download
|
|
121
|
+
- Template: `Invoice [PDF]`
|
|
122
|
+
- Payload: `{ "order_id": "{{ $json.id }}", "customer_name": "{{ $json.customer }}", "total": "{{ $json.total }}" }`
|
|
123
|
+
- Filename: `invoice-{{order_id}}.pdf`
|
|
124
|
+
3. **Google Drive** → Upload File
|
|
125
|
+
- File Name: `{{ $json.filename }}`
|
|
126
|
+
- Binary Property: `data`
|
|
127
|
+
|
|
128
|
+
## Resources
|
|
129
|
+
|
|
130
|
+
- [PDFKraft](https://pdfkraft.com)
|
|
131
|
+
- [n8n Community Nodes documentation](https://docs.n8n.io/integrations/community-nodes/)
|
|
@@ -4,6 +4,7 @@ export declare class PDFKraft implements INodeType {
|
|
|
4
4
|
methods: {
|
|
5
5
|
loadOptions: {
|
|
6
6
|
getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
getTemplateSampleData(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
8
|
};
|
|
8
9
|
};
|
|
9
10
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
@@ -8,6 +8,32 @@ const MIME_TYPES = {
|
|
|
8
8
|
jpg: 'image/jpeg',
|
|
9
9
|
webp: 'image/webp',
|
|
10
10
|
};
|
|
11
|
+
async function apiRequest(helpers, method, url, headers, body) {
|
|
12
|
+
return helpers.request({
|
|
13
|
+
method,
|
|
14
|
+
url,
|
|
15
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
16
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
17
|
+
json: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function pollUntilDone(helpers, baseUrl, headers, documentId, timeoutSeconds, node, itemIndex) {
|
|
21
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
22
|
+
let doc = {};
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
25
|
+
doc = await apiRequest(helpers, 'GET', `${baseUrl}/document_cards/${documentId}`, headers);
|
|
26
|
+
if (doc.status === 'success' || doc.status === 'failure')
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (doc.status !== 'success' && doc.status !== 'failure') {
|
|
30
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Document ${documentId} did not complete within ${timeoutSeconds}s`, { itemIndex });
|
|
31
|
+
}
|
|
32
|
+
if (doc.status === 'failure') {
|
|
33
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Document generation failed: ${doc.failure_cause ?? 'unknown error'}`, { itemIndex });
|
|
34
|
+
}
|
|
35
|
+
return doc;
|
|
36
|
+
}
|
|
11
37
|
class PDFKraft {
|
|
12
38
|
constructor() {
|
|
13
39
|
this.description = {
|
|
@@ -23,6 +49,7 @@ class PDFKraft {
|
|
|
23
49
|
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
24
50
|
credentials: [{ name: 'pdfKraftApi', required: true }],
|
|
25
51
|
properties: [
|
|
52
|
+
// ── Operation ────────────────────────────────────────────────────────
|
|
26
53
|
{
|
|
27
54
|
displayName: 'Operation',
|
|
28
55
|
name: 'operation',
|
|
@@ -32,15 +59,21 @@ class PDFKraft {
|
|
|
32
59
|
{
|
|
33
60
|
name: 'Generate Document',
|
|
34
61
|
value: 'generateDocument',
|
|
35
|
-
description: 'Queue a document for generation and return immediately with
|
|
62
|
+
description: 'Queue a document for generation and return immediately with the document ID',
|
|
36
63
|
action: 'Generate a document',
|
|
37
64
|
},
|
|
38
65
|
{
|
|
39
66
|
name: 'Generate Document and Wait',
|
|
40
67
|
value: 'generateAndWait',
|
|
41
|
-
description: 'Generate a document and poll until it is complete — returns the final result
|
|
68
|
+
description: 'Generate a document and poll until it is complete — returns the final result including the download URL',
|
|
42
69
|
action: 'Generate a document and wait for result',
|
|
43
70
|
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Generate Document and Download',
|
|
73
|
+
value: 'generateAndDownload',
|
|
74
|
+
description: 'Generate a document, wait for it to complete, and download the file as binary data in a single step',
|
|
75
|
+
action: 'Generate a document and download the file',
|
|
76
|
+
},
|
|
44
77
|
{
|
|
45
78
|
name: 'Get Document',
|
|
46
79
|
value: 'getDocument',
|
|
@@ -56,7 +89,7 @@ class PDFKraft {
|
|
|
56
89
|
{
|
|
57
90
|
name: 'List Documents',
|
|
58
91
|
value: 'listDocuments',
|
|
59
|
-
description: 'List
|
|
92
|
+
description: 'List generated documents for your account',
|
|
60
93
|
action: 'List documents',
|
|
61
94
|
},
|
|
62
95
|
{
|
|
@@ -76,18 +109,29 @@ class PDFKraft {
|
|
|
76
109
|
typeOptions: { loadOptionsMethod: 'getTemplates' },
|
|
77
110
|
default: '',
|
|
78
111
|
required: true,
|
|
79
|
-
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
|
|
112
|
+
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
|
|
80
113
|
description: 'The template to generate from. Only published templates appear. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
81
114
|
},
|
|
82
|
-
|
|
115
|
+
{
|
|
116
|
+
displayName: 'Template Variables',
|
|
117
|
+
name: 'templateVarsRef',
|
|
118
|
+
type: 'options',
|
|
119
|
+
typeOptions: {
|
|
120
|
+
loadOptionsMethod: 'getTemplateSampleData',
|
|
121
|
+
loadOptionsDependsOn: ['templateId'],
|
|
122
|
+
},
|
|
123
|
+
default: '',
|
|
124
|
+
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
|
|
125
|
+
description: 'Reference — open this dropdown to see what variables the selected template expects, along with their sample values. This field does not affect output.',
|
|
126
|
+
},
|
|
127
|
+
// ── Payload / filename ───────────────────────────────────────────────
|
|
83
128
|
{
|
|
84
129
|
displayName: 'Payload',
|
|
85
130
|
name: 'payload',
|
|
86
131
|
type: 'json',
|
|
87
132
|
default: '{}',
|
|
88
|
-
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
|
|
89
|
-
description: 'JSON
|
|
90
|
-
hint: 'Tip: use n8n expressions to pass data from previous nodes, e.g. <code>{{ $json.customer_name }}</code>',
|
|
133
|
+
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
|
|
134
|
+
description: 'JSON data to merge into the template. Use <code>{{ variable_name }}</code> syntax in your template. Open the "Template Variables" dropdown above to see expected fields.',
|
|
91
135
|
},
|
|
92
136
|
{
|
|
93
137
|
displayName: 'Filename',
|
|
@@ -95,37 +139,46 @@ class PDFKraft {
|
|
|
95
139
|
type: 'string',
|
|
96
140
|
default: '',
|
|
97
141
|
placeholder: 'invoice-{{order_id}}.pdf',
|
|
98
|
-
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
|
|
99
|
-
description: 'Output filename. Supports {{variable}} placeholders
|
|
142
|
+
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
|
|
143
|
+
description: 'Output filename. Supports <code>{{variable}}</code> placeholders from the payload. Leave blank to use the template default.',
|
|
144
|
+
},
|
|
145
|
+
// ── Wait / download options ──────────────────────────────────────────
|
|
146
|
+
{
|
|
147
|
+
displayName: 'Timeout (Seconds)',
|
|
148
|
+
name: 'timeoutSeconds',
|
|
149
|
+
type: 'number',
|
|
150
|
+
default: 60,
|
|
151
|
+
typeOptions: { minValue: 5, maxValue: 300 },
|
|
152
|
+
displayOptions: { show: { operation: ['generateAndWait', 'generateAndDownload'] } },
|
|
153
|
+
description: 'How long to wait for the document to complete before failing',
|
|
100
154
|
},
|
|
155
|
+
{
|
|
156
|
+
displayName: 'Output Field Name',
|
|
157
|
+
name: 'binaryPropertyName',
|
|
158
|
+
type: 'string',
|
|
159
|
+
default: 'data',
|
|
160
|
+
displayOptions: { show: { operation: ['generateAndDownload', 'downloadDocument'] } },
|
|
161
|
+
description: 'Name of the binary output field that will contain the downloaded file',
|
|
162
|
+
},
|
|
163
|
+
// ── Additional options (collapsible) ─────────────────────────────────
|
|
101
164
|
{
|
|
102
165
|
displayName: 'Additional Options',
|
|
103
166
|
name: 'additionalOptions',
|
|
104
167
|
type: 'collection',
|
|
105
168
|
placeholder: 'Add option',
|
|
106
169
|
default: {},
|
|
107
|
-
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait'] } },
|
|
170
|
+
displayOptions: { show: { operation: ['generateDocument', 'generateAndWait', 'generateAndDownload'] } },
|
|
108
171
|
options: [
|
|
109
172
|
{
|
|
110
173
|
displayName: 'Meta',
|
|
111
174
|
name: 'meta',
|
|
112
175
|
type: 'json',
|
|
113
176
|
default: '{}',
|
|
114
|
-
description: 'Arbitrary metadata to attach to the document
|
|
177
|
+
description: 'Arbitrary JSON metadata to attach to the document, returned in webhook payloads and API responses. Useful for tracking order IDs, user IDs, etc.',
|
|
115
178
|
},
|
|
116
179
|
],
|
|
117
180
|
},
|
|
118
|
-
// ──
|
|
119
|
-
{
|
|
120
|
-
displayName: 'Timeout (Seconds)',
|
|
121
|
-
name: 'timeoutSeconds',
|
|
122
|
-
type: 'number',
|
|
123
|
-
default: 60,
|
|
124
|
-
typeOptions: { minValue: 5, maxValue: 300 },
|
|
125
|
-
displayOptions: { show: { operation: ['generateAndWait'] } },
|
|
126
|
-
description: 'How long to wait for the document to complete before failing',
|
|
127
|
-
},
|
|
128
|
-
// ── Document ID fields ───────────────────────────────────────────────
|
|
181
|
+
// ── Get / Download by ID ─────────────────────────────────────────────
|
|
129
182
|
{
|
|
130
183
|
displayName: 'Document ID',
|
|
131
184
|
name: 'documentId',
|
|
@@ -136,16 +189,24 @@ class PDFKraft {
|
|
|
136
189
|
displayOptions: { show: { operation: ['getDocument', 'downloadDocument'] } },
|
|
137
190
|
description: 'The document ID returned by the Generate Document operation',
|
|
138
191
|
},
|
|
139
|
-
// ──
|
|
192
|
+
// ── List documents ───────────────────────────────────────────────────
|
|
140
193
|
{
|
|
141
|
-
displayName: '
|
|
142
|
-
name: '
|
|
143
|
-
type: '
|
|
144
|
-
default:
|
|
145
|
-
displayOptions: { show: { operation: ['
|
|
146
|
-
description: '
|
|
194
|
+
displayName: 'Return All',
|
|
195
|
+
name: 'returnAll',
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
default: false,
|
|
198
|
+
displayOptions: { show: { operation: ['listDocuments'] } },
|
|
199
|
+
description: 'Whether to return all pages of results or just the first page',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
displayName: 'Limit',
|
|
203
|
+
name: 'limit',
|
|
204
|
+
type: 'number',
|
|
205
|
+
default: 20,
|
|
206
|
+
typeOptions: { minValue: 1, maxValue: 100 },
|
|
207
|
+
displayOptions: { show: { operation: ['listDocuments'], returnAll: [false] } },
|
|
208
|
+
description: 'Maximum number of documents to return',
|
|
147
209
|
},
|
|
148
|
-
// ── List documents options ───────────────────────────────────────────
|
|
149
210
|
{
|
|
150
211
|
displayName: 'Status Filter',
|
|
151
212
|
name: 'statusFilter',
|
|
@@ -153,22 +214,13 @@ class PDFKraft {
|
|
|
153
214
|
options: [
|
|
154
215
|
{ name: 'All', value: '' },
|
|
155
216
|
{ name: 'Success', value: 'success' },
|
|
156
|
-
{ name: 'Failure', value: 'failure' },
|
|
157
217
|
{ name: 'Pending', value: 'pending' },
|
|
218
|
+
{ name: 'Failure', value: 'failure' },
|
|
158
219
|
],
|
|
159
220
|
default: '',
|
|
160
221
|
displayOptions: { show: { operation: ['listDocuments'] } },
|
|
161
222
|
description: 'Filter documents by status',
|
|
162
223
|
},
|
|
163
|
-
{
|
|
164
|
-
displayName: 'Limit',
|
|
165
|
-
name: 'limit',
|
|
166
|
-
type: 'number',
|
|
167
|
-
default: 20,
|
|
168
|
-
typeOptions: { minValue: 1, maxValue: 100 },
|
|
169
|
-
displayOptions: { show: { operation: ['listDocuments'] } },
|
|
170
|
-
description: 'Maximum number of documents to return',
|
|
171
|
-
},
|
|
172
224
|
],
|
|
173
225
|
};
|
|
174
226
|
this.methods = {
|
|
@@ -185,14 +237,38 @@ class PDFKraft {
|
|
|
185
237
|
});
|
|
186
238
|
const templates = (response.data ?? []).filter(t => t.published_at);
|
|
187
239
|
if (templates.length === 0) {
|
|
188
|
-
return [{ name: 'No published templates
|
|
240
|
+
return [{ name: '(No published templates — publish one in PDFKraft first)', value: '' }];
|
|
189
241
|
}
|
|
190
242
|
return templates.map(t => ({
|
|
191
|
-
name: `${t.name}
|
|
243
|
+
name: `${t.name} [${t.output_type.toUpperCase()}]`,
|
|
192
244
|
value: t.id,
|
|
193
245
|
description: t.identifier,
|
|
194
246
|
}));
|
|
195
247
|
},
|
|
248
|
+
async getTemplateSampleData() {
|
|
249
|
+
const templateId = this.getCurrentNodeParameter('templateId');
|
|
250
|
+
if (!templateId)
|
|
251
|
+
return [{ name: '← Select a template first', value: '' }];
|
|
252
|
+
const credentials = await this.getCredentials('pdfKraftApi');
|
|
253
|
+
const baseUrl = credentials.baseUrl.replace(/\/$/, '');
|
|
254
|
+
const apiKey = credentials.apiKey;
|
|
255
|
+
const response = await this.helpers.request({
|
|
256
|
+
method: 'GET',
|
|
257
|
+
url: `${baseUrl}/document_templates/${templateId}`,
|
|
258
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
259
|
+
json: true,
|
|
260
|
+
});
|
|
261
|
+
const sampleData = response.sample_data ?? response.sample_data_draft ?? {};
|
|
262
|
+
const entries = Object.entries(sampleData);
|
|
263
|
+
if (entries.length === 0) {
|
|
264
|
+
return [{ name: '(No sample data defined — add it in the template editor under Sample Data)', value: '' }];
|
|
265
|
+
}
|
|
266
|
+
return entries.map(([key, value]) => ({
|
|
267
|
+
name: key,
|
|
268
|
+
value: key,
|
|
269
|
+
description: `Sample: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`,
|
|
270
|
+
}));
|
|
271
|
+
},
|
|
196
272
|
},
|
|
197
273
|
};
|
|
198
274
|
}
|
|
@@ -202,11 +278,12 @@ class PDFKraft {
|
|
|
202
278
|
const credentials = await this.getCredentials('pdfKraftApi');
|
|
203
279
|
const baseUrl = credentials.baseUrl.replace(/\/$/, '');
|
|
204
280
|
const apiKey = credentials.apiKey;
|
|
205
|
-
const
|
|
281
|
+
const authHeaders = { Authorization: `Bearer ${apiKey}` };
|
|
282
|
+
const jsonHeaders = { ...authHeaders, 'Content-Type': 'application/json' };
|
|
206
283
|
for (let i = 0; i < items.length; i++) {
|
|
207
284
|
const operation = this.getNodeParameter('operation', i);
|
|
208
|
-
// ──
|
|
209
|
-
|
|
285
|
+
// ── Shared: build generate body ────────────────────────────────────
|
|
286
|
+
const buildGenerateBody = () => {
|
|
210
287
|
const templateId = this.getNodeParameter('templateId', i);
|
|
211
288
|
const payloadRaw = this.getNodeParameter('payload', i);
|
|
212
289
|
const filename = this.getNodeParameter('filename', i);
|
|
@@ -218,82 +295,61 @@ class PDFKraft {
|
|
|
218
295
|
const body = { document_template_id: templateId, payload, meta };
|
|
219
296
|
if (filename)
|
|
220
297
|
body.filename = filename;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
json: true,
|
|
227
|
-
});
|
|
298
|
+
return body;
|
|
299
|
+
};
|
|
300
|
+
// ── Generate Document (fire and forget) ────────────────────────────
|
|
301
|
+
if (operation === 'generateDocument') {
|
|
302
|
+
const response = await apiRequest(this.helpers, 'POST', `${baseUrl}/documents`, jsonHeaders, buildGenerateBody());
|
|
228
303
|
results.push({ json: response });
|
|
229
304
|
}
|
|
230
|
-
// ── Generate and Wait
|
|
305
|
+
// ── Generate and Wait ──────────────────────────────────────────────
|
|
231
306
|
if (operation === 'generateAndWait') {
|
|
232
|
-
const templateId = this.getNodeParameter('templateId', i);
|
|
233
|
-
const payloadRaw = this.getNodeParameter('payload', i);
|
|
234
|
-
const filename = this.getNodeParameter('filename', i);
|
|
235
|
-
const additionalOptions = this.getNodeParameter('additionalOptions', i);
|
|
236
307
|
const timeoutSeconds = this.getNodeParameter('timeoutSeconds', i);
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
? (typeof additionalOptions.meta === 'string' ? JSON.parse(additionalOptions.meta) : additionalOptions.meta)
|
|
240
|
-
: {};
|
|
241
|
-
const body = { document_template_id: templateId, payload, meta };
|
|
242
|
-
if (filename)
|
|
243
|
-
body.filename = filename;
|
|
244
|
-
const created = await this.helpers.request({
|
|
245
|
-
method: 'POST',
|
|
246
|
-
url: `${baseUrl}/documents`,
|
|
247
|
-
headers,
|
|
248
|
-
body: JSON.stringify(body),
|
|
249
|
-
json: true,
|
|
250
|
-
});
|
|
251
|
-
const documentId = created.id;
|
|
252
|
-
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
253
|
-
let doc = {};
|
|
254
|
-
while (Date.now() < deadline) {
|
|
255
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
256
|
-
doc = await this.helpers.request({
|
|
257
|
-
method: 'GET',
|
|
258
|
-
url: `${baseUrl}/document_cards/${documentId}`,
|
|
259
|
-
headers,
|
|
260
|
-
json: true,
|
|
261
|
-
});
|
|
262
|
-
if (doc.status === 'success' || doc.status === 'failure')
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
if (doc.status !== 'success' && doc.status !== 'failure') {
|
|
266
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document ${documentId} did not complete within ${timeoutSeconds}s`, { itemIndex: i });
|
|
267
|
-
}
|
|
268
|
-
if (doc.status === 'failure') {
|
|
269
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document generation failed: ${doc.failure_cause ?? 'unknown error'}`, { itemIndex: i });
|
|
270
|
-
}
|
|
308
|
+
const created = await apiRequest(this.helpers, 'POST', `${baseUrl}/documents`, jsonHeaders, buildGenerateBody());
|
|
309
|
+
const doc = await pollUntilDone(this.helpers, baseUrl, jsonHeaders, created.id, timeoutSeconds, this.getNode(), i);
|
|
271
310
|
results.push({ json: doc });
|
|
272
311
|
}
|
|
312
|
+
// ── Generate and Download ──────────────────────────────────────────
|
|
313
|
+
if (operation === 'generateAndDownload') {
|
|
314
|
+
const timeoutSeconds = this.getNodeParameter('timeoutSeconds', i);
|
|
315
|
+
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
|
316
|
+
const created = await apiRequest(this.helpers, 'POST', `${baseUrl}/documents`, jsonHeaders, buildGenerateBody());
|
|
317
|
+
const doc = await pollUntilDone(this.helpers, baseUrl, jsonHeaders, created.id, timeoutSeconds, this.getNode(), i);
|
|
318
|
+
const outputType = doc.output_type ?? 'pdf';
|
|
319
|
+
const filename = doc.filename ?? `document.${outputType}`;
|
|
320
|
+
const mimeType = MIME_TYPES[outputType] ?? 'application/octet-stream';
|
|
321
|
+
const buffer = await this.helpers.request({
|
|
322
|
+
method: 'GET',
|
|
323
|
+
url: `${baseUrl}/documents/${doc.id}/download`,
|
|
324
|
+
headers: authHeaders,
|
|
325
|
+
encoding: null,
|
|
326
|
+
resolveWithFullResponse: false,
|
|
327
|
+
});
|
|
328
|
+
const binaryData = await this.helpers.prepareBinaryData(buffer, filename, mimeType);
|
|
329
|
+
results.push({
|
|
330
|
+
json: {
|
|
331
|
+
id: doc.id,
|
|
332
|
+
filename,
|
|
333
|
+
output_type: outputType,
|
|
334
|
+
download_url: doc.download_url,
|
|
335
|
+
generated_at: doc.generated_at,
|
|
336
|
+
},
|
|
337
|
+
binary: { [binaryPropertyName]: binaryData },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
273
340
|
// ── Get Document ───────────────────────────────────────────────────
|
|
274
341
|
if (operation === 'getDocument') {
|
|
275
342
|
const documentId = this.getNodeParameter('documentId', i);
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
url: `${baseUrl}/document_cards/${documentId}`,
|
|
279
|
-
headers,
|
|
280
|
-
json: true,
|
|
281
|
-
});
|
|
282
|
-
results.push({ json: response });
|
|
343
|
+
const doc = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_cards/${documentId}`, jsonHeaders);
|
|
344
|
+
results.push({ json: doc });
|
|
283
345
|
}
|
|
284
346
|
// ── Download Document ──────────────────────────────────────────────
|
|
285
347
|
if (operation === 'downloadDocument') {
|
|
286
348
|
const documentId = this.getNodeParameter('documentId', i);
|
|
287
349
|
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
|
288
|
-
|
|
289
|
-
const doc = await this.helpers.request({
|
|
290
|
-
method: 'GET',
|
|
291
|
-
url: `${baseUrl}/document_cards/${documentId}`,
|
|
292
|
-
headers,
|
|
293
|
-
json: true,
|
|
294
|
-
});
|
|
350
|
+
const doc = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_cards/${documentId}`, jsonHeaders);
|
|
295
351
|
if (doc.status !== 'success') {
|
|
296
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document is not ready — status is "${doc.status}"`, { itemIndex: i });
|
|
352
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Document is not ready — current status is "${doc.status}"`, { itemIndex: i });
|
|
297
353
|
}
|
|
298
354
|
const outputType = doc.output_type ?? 'pdf';
|
|
299
355
|
const filename = doc.filename ?? `document.${outputType}`;
|
|
@@ -301,38 +357,43 @@ class PDFKraft {
|
|
|
301
357
|
const buffer = await this.helpers.request({
|
|
302
358
|
method: 'GET',
|
|
303
359
|
url: `${baseUrl}/documents/${documentId}/download`,
|
|
304
|
-
headers:
|
|
360
|
+
headers: authHeaders,
|
|
305
361
|
encoding: null,
|
|
306
362
|
resolveWithFullResponse: false,
|
|
307
363
|
});
|
|
308
364
|
const binaryData = await this.helpers.prepareBinaryData(buffer, filename, mimeType);
|
|
309
|
-
results.push({
|
|
365
|
+
results.push({
|
|
366
|
+
json: { documentId, filename, output_type: outputType, download_url: doc.download_url },
|
|
367
|
+
binary: { [binaryPropertyName]: binaryData },
|
|
368
|
+
});
|
|
310
369
|
}
|
|
311
370
|
// ── List Documents ─────────────────────────────────────────────────
|
|
312
371
|
if (operation === 'listDocuments') {
|
|
372
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
313
373
|
const statusFilter = this.getNodeParameter('statusFilter', i);
|
|
314
|
-
const limit = this.getNodeParameter('limit', i);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
374
|
+
const limit = returnAll ? Infinity : this.getNodeParameter('limit', i);
|
|
375
|
+
let page = 1;
|
|
376
|
+
let collected = 0;
|
|
377
|
+
let hasMore = true;
|
|
378
|
+
while (hasMore && collected < limit) {
|
|
379
|
+
const params = new URLSearchParams({ page: String(page) });
|
|
380
|
+
if (statusFilter)
|
|
381
|
+
params.set('status', statusFilter);
|
|
382
|
+
const response = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_cards?${params}`, jsonHeaders);
|
|
383
|
+
const docs = response.data ?? [];
|
|
384
|
+
const remaining = limit - collected;
|
|
385
|
+
for (const doc of docs.slice(0, remaining)) {
|
|
386
|
+
results.push({ json: doc });
|
|
387
|
+
collected++;
|
|
388
|
+
}
|
|
389
|
+
const totalPages = response.meta?.total_pages ?? 1;
|
|
390
|
+
hasMore = page < totalPages && docs.length > 0;
|
|
391
|
+
page++;
|
|
392
|
+
}
|
|
327
393
|
}
|
|
328
394
|
// ── List Templates ─────────────────────────────────────────────────
|
|
329
395
|
if (operation === 'listTemplates') {
|
|
330
|
-
const response = await this.helpers
|
|
331
|
-
method: 'GET',
|
|
332
|
-
url: `${baseUrl}/document_template_cards`,
|
|
333
|
-
headers,
|
|
334
|
-
json: true,
|
|
335
|
-
});
|
|
396
|
+
const response = await apiRequest(this.helpers, 'GET', `${baseUrl}/document_template_cards`, jsonHeaders);
|
|
336
397
|
for (const t of response.data ?? [])
|
|
337
398
|
results.push({ json: t });
|
|
338
399
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { IHookFunctions, IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
|
|
1
|
+
import type { IHookFunctions, IWebhookFunctions, ILoadOptionsFunctions, INodePropertyOptions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
|
|
2
2
|
export declare class PDFKraftTrigger implements INodeType {
|
|
3
3
|
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
4
9
|
webhookMethods: {
|
|
5
10
|
default: {
|
|
6
11
|
checkExists(this: IHookFunctions): Promise<boolean>;
|
|
@@ -27,16 +27,69 @@ class PDFKraftTrigger {
|
|
|
27
27
|
type: 'string',
|
|
28
28
|
typeOptions: { password: true },
|
|
29
29
|
default: '',
|
|
30
|
-
|
|
30
|
+
required: true,
|
|
31
|
+
description: 'The secret shown when you created the webhook endpoint in PDFKraft (Settings → Webhooks → Create Endpoint)',
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
displayName: 'Verify Signature',
|
|
34
35
|
name: 'verifySignature',
|
|
35
36
|
type: 'boolean',
|
|
36
37
|
default: true,
|
|
38
|
+
description: 'Whether to verify the HMAC signature on incoming requests. Recommended to leave enabled.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Event Types',
|
|
42
|
+
name: 'eventTypes',
|
|
43
|
+
type: 'multiOptions',
|
|
44
|
+
options: [
|
|
45
|
+
{
|
|
46
|
+
name: 'Document Succeeded',
|
|
47
|
+
value: 'document.success',
|
|
48
|
+
description: 'Trigger when a document is generated successfully',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'Document Failed',
|
|
52
|
+
value: 'document.failure',
|
|
53
|
+
description: 'Trigger when a document generation fails',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
default: ['document.success'],
|
|
57
|
+
description: 'Which event types should trigger this node. Leave both selected to receive all events.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
displayName: 'Template Filter',
|
|
61
|
+
name: 'templateFilter',
|
|
62
|
+
type: 'options',
|
|
63
|
+
typeOptions: { loadOptionsMethod: 'getTemplates' },
|
|
64
|
+
default: '*',
|
|
65
|
+
description: 'Only trigger for documents generated from this template. Select "All Templates" to receive events from every template.',
|
|
37
66
|
},
|
|
38
67
|
],
|
|
39
68
|
};
|
|
69
|
+
this.methods = {
|
|
70
|
+
loadOptions: {
|
|
71
|
+
async getTemplates() {
|
|
72
|
+
const credentials = await this.getCredentials('pdfKraftApi');
|
|
73
|
+
const baseUrl = credentials.baseUrl.replace(/\/$/, '');
|
|
74
|
+
const apiKey = credentials.apiKey;
|
|
75
|
+
const response = await this.helpers.request({
|
|
76
|
+
method: 'GET',
|
|
77
|
+
url: `${baseUrl}/document_template_cards`,
|
|
78
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
79
|
+
json: true,
|
|
80
|
+
});
|
|
81
|
+
const templates = (response.data ?? []).filter(t => t.published_at);
|
|
82
|
+
return [
|
|
83
|
+
{ name: 'All Templates', value: '*' },
|
|
84
|
+
...templates.map(t => ({
|
|
85
|
+
name: `${t.name} [${t.output_type.toUpperCase()}]`,
|
|
86
|
+
value: t.identifier,
|
|
87
|
+
description: t.identifier,
|
|
88
|
+
})),
|
|
89
|
+
];
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
40
93
|
this.webhookMethods = {
|
|
41
94
|
default: {
|
|
42
95
|
async checkExists() { return false; },
|
|
@@ -49,20 +102,37 @@ class PDFKraftTrigger {
|
|
|
49
102
|
const req = this.getRequestObject();
|
|
50
103
|
const verifySignature = this.getNodeParameter('verifySignature');
|
|
51
104
|
const secret = this.getNodeParameter('webhookSecret');
|
|
105
|
+
const eventTypes = this.getNodeParameter('eventTypes');
|
|
106
|
+
const templateFilter = this.getNodeParameter('templateFilter');
|
|
107
|
+
// ── Signature verification ─────────────────────────────────────────
|
|
52
108
|
if (verifySignature && secret) {
|
|
53
109
|
const signature = req.headers['x-pdfkraft-signature'];
|
|
54
110
|
const timestamp = req.headers['x-pdfkraft-timestamp'];
|
|
55
111
|
const rawBody = JSON.stringify(req.body);
|
|
56
112
|
const expected = crypto_1.default
|
|
57
113
|
.createHmac('sha256', secret)
|
|
58
|
-
.update(`${timestamp}.${rawBody}`)
|
|
114
|
+
.update(`${timestamp ?? ''}.${rawBody}`)
|
|
59
115
|
.digest('hex');
|
|
60
|
-
|
|
61
|
-
|
|
116
|
+
const sigBuf = Buffer.from(signature ?? '', 'hex');
|
|
117
|
+
const expBuf = Buffer.from(expected, 'hex');
|
|
118
|
+
if (sigBuf.length !== expBuf.length || !crypto_1.default.timingSafeEqual(sigBuf, expBuf)) {
|
|
119
|
+
return { webhookResponse: { statusCode: 401, body: { error: 'invalid signature' } } };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const body = req.body;
|
|
123
|
+
// ── Event type filter ──────────────────────────────────────────────
|
|
124
|
+
if (eventTypes.length > 0 && body.event && !eventTypes.includes(body.event)) {
|
|
125
|
+
return { webhookResponse: { statusCode: 200, body: { ignored: true, reason: 'event_type_filtered' } } };
|
|
126
|
+
}
|
|
127
|
+
// ── Template filter ────────────────────────────────────────────────
|
|
128
|
+
if (templateFilter && templateFilter !== '*') {
|
|
129
|
+
const docIdentifier = body.document?.document_template_identifier;
|
|
130
|
+
if (docIdentifier !== templateFilter) {
|
|
131
|
+
return { webhookResponse: { statusCode: 200, body: { ignored: true, reason: 'template_filtered' } } };
|
|
62
132
|
}
|
|
63
133
|
}
|
|
64
134
|
return {
|
|
65
|
-
workflowData: [[{ json:
|
|
135
|
+
workflowData: [[{ json: body }]],
|
|
66
136
|
};
|
|
67
137
|
}
|
|
68
138
|
}
|
package/package.json
CHANGED