n8n-nodes-sotoros-gotenberg 1.0.2 → 1.0.4

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.
@@ -72,12 +72,20 @@ class Gotenberg {
72
72
  default: 'office',
73
73
  required: true,
74
74
  },
75
+ {
76
+ displayName: 'List Property',
77
+ name: 'listPropertyName',
78
+ type: 'string',
79
+ default: 'items',
80
+ description: 'Name of the JSON property that contains the array/list of items from the aggregation node (e.g., "items", "data")',
81
+ },
75
82
  {
76
83
  displayName: 'Binary Property',
77
84
  name: 'binaryPropertyName',
78
85
  type: 'string',
79
- default: '',
80
- description: 'Name of the binary property that contains the file(s) to convert. If empty, all binary properties from the previous node will be processed.',
86
+ default: 'data',
87
+ description: 'Name of the binary property in each item of the list that contains the file(s) to convert',
88
+ required: true,
81
89
  },
82
90
  {
83
91
  displayName: 'Output Binary Property',
@@ -138,13 +146,44 @@ class Gotenberg {
138
146
  };
139
147
  }
140
148
  async execute() {
149
+ // Работаем с одним элементом, который содержит список (результат узла агрегации)
141
150
  const items = this.getInputData();
142
151
  const returnData = [];
143
- const gotenbergUrl = this.getNodeParameter('gotenbergUrl', 0);
144
- const operation = this.getNodeParameter('operation', 0);
145
- const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) || '';
146
- const outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', 0);
147
- const options = this.getNodeParameter('options', 0, {});
152
+ if (items.length === 0) {
153
+ return [returnData];
154
+ }
155
+ // Берем первый элемент (результат узла агрегации)
156
+ const aggregatedItem = items[0];
157
+ let gotenbergUrl;
158
+ let operation;
159
+ let listPropertyName;
160
+ let binaryPropertyName;
161
+ let outputBinaryPropertyName;
162
+ let options;
163
+ try {
164
+ gotenbergUrl = this.getNodeParameter('gotenbergUrl', 0);
165
+ operation = this.getNodeParameter('operation', 0);
166
+ listPropertyName = this.getNodeParameter('listPropertyName', 0);
167
+ binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0);
168
+ outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', 0);
169
+ options = this.getNodeParameter('options', 0, {});
170
+ }
171
+ catch (error) {
172
+ if (this.continueOnFail()) {
173
+ returnData.push({
174
+ json: {
175
+ ...aggregatedItem.json,
176
+ error: error instanceof Error ? error.message : String(error),
177
+ success: false,
178
+ },
179
+ binary: aggregatedItem.binary,
180
+ });
181
+ return [returnData];
182
+ }
183
+ else {
184
+ throw error;
185
+ }
186
+ }
148
187
  // Вспомогательная функция для получения расширения файла из MIME типа
149
188
  const getFileExtensionFromMimeType = (mimeType) => {
150
189
  const mimeToExt = {
@@ -160,174 +199,216 @@ class Gotenberg {
160
199
  };
161
200
  return mimeToExt[mimeType] || 'bin';
162
201
  };
163
- for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
164
- try {
165
- const item = items[itemIndex];
166
- const binaryData = item.binary;
167
- if (!binaryData || Object.keys(binaryData).length === 0) {
168
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No binary data found in item ${itemIndex}. Please make sure the binary property exists.`, { itemIndex });
202
+ try {
203
+ // Извлекаем список из JSON свойства агрегированного элемента
204
+ const listData = aggregatedItem.json[listPropertyName];
205
+ if (!listData) {
206
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Property "${listPropertyName}" not found in aggregated item. Please check the property name.`);
207
+ }
208
+ if (!Array.isArray(listData)) {
209
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Property "${listPropertyName}" is not an array. Expected an array of items from aggregation node.`);
210
+ }
211
+ if (listData.length === 0) {
212
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `The list in property "${listPropertyName}" is empty. No items to process.`);
213
+ }
214
+ // Создаем FormData для отправки в Gotenberg - один запрос для всех элементов списка
215
+ const formData = new form_data_1.default();
216
+ let totalFilesCount = 0;
217
+ // Собираем все binary данные из всех элементов списка
218
+ for (let listIndex = 0; listIndex < listData.length; listIndex++) {
219
+ const listItem = listData[listIndex];
220
+ if (!listItem || typeof listItem !== 'object') {
221
+ continue; // Пропускаем некорректные элементы
169
222
  }
170
- // Создаем FormData для отправки в Gotenberg
171
- const formData = new form_data_1.default();
172
- let totalFilesCount = 0;
173
- // Определяем, какие binary свойства обрабатывать
174
- const binaryPropertiesToProcess = binaryPropertyName
175
- ? [binaryPropertyName]
176
- : Object.keys(binaryData);
177
- // Обрабатываем все указанные binary свойства
178
- for (const propName of binaryPropertiesToProcess) {
179
- const binaryProperty = binaryData[propName];
180
- if (!binaryProperty) {
181
- if (binaryPropertyName) {
182
- // Если явно указано свойство, но его нет - ошибка
183
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Binary property "${propName}" not found in item ${itemIndex}`, { itemIndex });
184
- }
185
- // Если не указано явно - просто пропускаем отсутствующие свойства
186
- continue;
187
- }
188
- // Обрабатываем как одиночное значение, так и массив
189
- const binaryItems = Array.isArray(binaryProperty) ? binaryProperty : [binaryProperty];
190
- // Добавляем все файлы из этого свойства
191
- for (let i = 0; i < binaryItems.length; i++) {
192
- const binaryItem = binaryItems[i];
193
- if (!binaryItem || !binaryItem.data) {
194
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Binary item ${i} in property "${propName}" is missing data`, { itemIndex });
195
- }
196
- // Получаем buffer из binary данных
197
- // В n8n, когда binary данные приходят как массив, каждый элемент уже содержит data в base64
198
- let dataBuffer;
199
- if (Array.isArray(binaryProperty)) {
200
- // Если это массив, декодируем base64 из элемента массива
201
- dataBuffer = Buffer.from(binaryItem.data, 'base64');
223
+ // Элемент списка может быть INodeExecutionData (с полями json и binary)
224
+ // или обычным объектом с binary данными в разных местах
225
+ let itemBinary;
226
+ // Вариант 1: элемент имеет структуру INodeExecutionData (json и binary)
227
+ if ('binary' in listItem && listItem.binary && typeof listItem.binary === 'object') {
228
+ itemBinary = listItem.binary;
229
+ }
230
+ // Вариант 2: binary данные находятся в свойстве элемента напрямую
231
+ else if (binaryPropertyName in listItem) {
232
+ const binaryProp = listItem[binaryPropertyName];
233
+ if (binaryProp && typeof binaryProp === 'object') {
234
+ // Может быть одиночным объектом или массивом
235
+ if (binaryProp.data) {
236
+ // Это одиночный binary объект
237
+ itemBinary = { [binaryPropertyName]: binaryProp };
202
238
  }
203
- else {
204
- // Если это одиночное значение, используем helper
205
- dataBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, propName);
239
+ else if (Array.isArray(binaryProp)) {
240
+ // Это массив binary объектов
241
+ itemBinary = { [binaryPropertyName]: binaryProp };
206
242
  }
207
- const fileName = binaryItem.fileName ||
208
- `${propName}_${i}.${getFileExtensionFromMimeType(binaryItem.mimeType || '')}`;
209
- formData.append('files', dataBuffer, {
210
- filename: fileName,
211
- contentType: binaryItem.mimeType || 'application/octet-stream',
212
- });
213
- totalFilesCount++;
214
243
  }
215
244
  }
216
- if (totalFilesCount === 0) {
217
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No valid binary files found in item ${itemIndex}`, { itemIndex });
245
+ if (!itemBinary || Object.keys(itemBinary).length === 0) {
246
+ continue; // Пропускаем элементы без binary данных
218
247
  }
219
- // Добавляем опции в зависимости от операции
220
- if (operation === 'office' || operation === 'html' || operation === 'markdown') {
221
- if (options.landscape) {
222
- formData.append('landscape', 'true');
223
- }
224
- if (options.pageRanges) {
225
- formData.append('pageRanges', options.pageRanges);
226
- }
227
- if (options.scale !== undefined) {
228
- formData.append('scale', options.scale.toString());
229
- }
230
- if (options.waitTimeout) {
231
- formData.append('waitTimeout', options.waitTimeout);
232
- }
233
- if (options.waitDelay) {
234
- formData.append('waitDelay', options.waitDelay);
248
+ // Получаем binary свойство
249
+ const binaryProperty = itemBinary[binaryPropertyName];
250
+ if (!binaryProperty) {
251
+ continue; // Пропускаем элементы без указанного binary свойства
252
+ }
253
+ // Обрабатываем как одиночное значение, так и массив
254
+ const binaryItems = Array.isArray(binaryProperty) ? binaryProperty : [binaryProperty];
255
+ // Добавляем все файлы из этого элемента списка
256
+ for (let i = 0; i < binaryItems.length; i++) {
257
+ const binaryItem = binaryItems[i];
258
+ if (!binaryItem || !binaryItem.data) {
259
+ continue; // Пропускаем файлы без данных
235
260
  }
261
+ // Получаем buffer из binary данных (всегда base64 в n8n)
262
+ const dataBuffer = Buffer.from(binaryItem.data, 'base64');
263
+ const fileName = binaryItem.fileName ||
264
+ `${binaryPropertyName}_${listIndex}_${i}.${getFileExtensionFromMimeType(binaryItem.mimeType || '')}`;
265
+ formData.append('files', dataBuffer, {
266
+ filename: fileName,
267
+ contentType: binaryItem.mimeType || 'application/octet-stream',
268
+ });
269
+ totalFilesCount++;
236
270
  }
237
- // Определяем endpoint в зависимости от операции
238
- let endpoint = '';
239
- switch (operation) {
240
- case 'office':
241
- endpoint = '/convert/office';
242
- break;
243
- case 'html':
244
- endpoint = '/convert/html';
245
- break;
246
- case 'markdown':
247
- endpoint = '/convert/markdown';
248
- break;
249
- case 'url':
250
- endpoint = '/convert/url';
251
- // Для URL операции нужен параметр url
252
- if (item.json.url) {
253
- formData.append('url', item.json.url);
254
- }
255
- break;
256
- case 'merge':
257
- endpoint = '/forms/pdfengines/merge';
258
- break;
259
- default:
260
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex });
271
+ }
272
+ if (totalFilesCount === 0) {
273
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No valid binary files found in any items. Please make sure binary data exists.');
274
+ }
275
+ // Добавляем опции в зависимости от операции
276
+ if (operation === 'office' || operation === 'html' || operation === 'markdown') {
277
+ if (options.landscape) {
278
+ formData.append('landscape', 'true');
261
279
  }
262
- // Отправляем запрос в Gotenberg
263
- const url = `${gotenbergUrl.replace(/\/$/, '')}${endpoint}`;
264
- const response = await this.helpers.httpRequest({
265
- method: 'POST',
266
- url,
267
- body: formData,
268
- returnFullResponse: true,
269
- headers: formData.getHeaders(),
270
- });
271
- // Проверяем статус ответа
272
- if (response.statusCode && response.statusCode >= 400) {
273
- let errorText;
274
- if (Buffer.isBuffer(response.body)) {
275
- errorText = response.body.toString('utf-8');
276
- }
277
- else if (typeof response.body === 'string') {
278
- errorText = response.body;
279
- }
280
- else {
281
- errorText = JSON.stringify(response.body);
282
- }
283
- throw new n8n_workflow_2.NodeApiError(this.getNode(), {
284
- message: `Gotenberg API error: ${response.statusCode}`,
285
- description: errorText,
286
- }, { itemIndex });
280
+ if (options.pageRanges) {
281
+ formData.append('pageRanges', options.pageRanges);
282
+ }
283
+ if (options.scale !== undefined) {
284
+ formData.append('scale', options.scale.toString());
285
+ }
286
+ if (options.waitTimeout) {
287
+ formData.append('waitTimeout', options.waitTimeout);
287
288
  }
288
- // Получаем PDF buffer
289
- let pdfBuffer;
289
+ if (options.waitDelay) {
290
+ formData.append('waitDelay', options.waitDelay);
291
+ }
292
+ }
293
+ // Определяем endpoint в зависимости от операции
294
+ let endpoint = '';
295
+ switch (operation) {
296
+ case 'office':
297
+ endpoint = '/convert/office';
298
+ break;
299
+ case 'html':
300
+ endpoint = '/convert/html';
301
+ break;
302
+ case 'markdown':
303
+ endpoint = '/convert/markdown';
304
+ break;
305
+ case 'url':
306
+ endpoint = '/convert/url';
307
+ // Для URL операции нужен параметр url из первого элемента списка
308
+ if (listData.length > 0 && listData[0] && typeof listData[0] === 'object' && 'url' in listData[0]) {
309
+ formData.append('url', listData[0].url);
310
+ }
311
+ break;
312
+ case 'merge':
313
+ endpoint = '/forms/pdfengines/merge';
314
+ break;
315
+ default:
316
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
317
+ }
318
+ // Отправляем запрос в Gotenberg
319
+ const url = `${gotenbergUrl.replace(/\/$/, '')}${endpoint}`;
320
+ const response = await this.helpers.httpRequest({
321
+ method: 'POST',
322
+ url,
323
+ body: formData,
324
+ returnFullResponse: true,
325
+ headers: formData.getHeaders(),
326
+ });
327
+ // Проверяем статус ответа
328
+ if (response.statusCode && response.statusCode >= 400) {
329
+ let errorText;
290
330
  if (Buffer.isBuffer(response.body)) {
291
- pdfBuffer = response.body;
331
+ errorText = response.body.toString('utf-8');
292
332
  }
293
333
  else if (typeof response.body === 'string') {
294
- pdfBuffer = Buffer.from(response.body, 'base64');
334
+ errorText = response.body;
295
335
  }
296
336
  else {
297
- // Если это ArrayBuffer или другой тип
298
- const bodyArray = new Uint8Array(response.body);
299
- pdfBuffer = Buffer.from(bodyArray);
337
+ errorText = JSON.stringify(response.body);
300
338
  }
301
- // Создаем выходной элемент
302
- const newItem = {
303
- json: {
304
- ...item.json,
305
- success: true,
306
- operation,
307
- fileCount: totalFilesCount,
339
+ throw new n8n_workflow_2.NodeApiError(this.getNode(), {
340
+ message: `Gotenberg API error: ${response.statusCode}`,
341
+ description: errorText,
342
+ });
343
+ }
344
+ // Получаем PDF buffer - Gotenberg возвращает PDF как binary данные
345
+ let pdfBuffer;
346
+ if (Buffer.isBuffer(response.body)) {
347
+ // Если это уже Buffer - используем напрямую
348
+ pdfBuffer = response.body;
349
+ }
350
+ else if (response.body instanceof ArrayBuffer) {
351
+ // Если это ArrayBuffer - конвертируем в Buffer
352
+ pdfBuffer = Buffer.from(response.body);
353
+ }
354
+ else if (typeof response.body === 'string') {
355
+ // Если это строка - это может быть base64 или raw binary
356
+ // Попробуем сначала как base64, если не получится - как raw
357
+ try {
358
+ pdfBuffer = Buffer.from(response.body, 'base64');
359
+ // Проверяем, что это действительно валидный PDF (начинается с %PDF)
360
+ if (pdfBuffer.length > 4 && pdfBuffer.toString('ascii', 0, 4) !== '%PDF') {
361
+ // Если не PDF, попробуем как raw binary
362
+ pdfBuffer = Buffer.from(response.body, 'binary');
363
+ }
364
+ }
365
+ catch {
366
+ // Если не получилось декодировать как base64, используем как raw binary
367
+ pdfBuffer = Buffer.from(response.body, 'binary');
368
+ }
369
+ }
370
+ else {
371
+ // Если это другой тип - конвертируем через Uint8Array
372
+ const bodyArray = new Uint8Array(response.body);
373
+ pdfBuffer = Buffer.from(bodyArray);
374
+ }
375
+ // Проверяем, что получили валидный PDF
376
+ if (pdfBuffer.length < 4 || pdfBuffer.toString('ascii', 0, 4) !== '%PDF') {
377
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid PDF received from Gotenberg. The response does not appear to be a valid PDF file.');
378
+ }
379
+ // Создаем один выходной элемент с результатом
380
+ const newItem = {
381
+ json: {
382
+ ...aggregatedItem.json,
383
+ success: true,
384
+ operation,
385
+ fileCount: totalFilesCount,
386
+ processedItems: listData.length,
387
+ },
388
+ binary: {
389
+ ...aggregatedItem.binary,
390
+ [outputBinaryPropertyName]: {
391
+ data: pdfBuffer.toString('base64'),
392
+ mimeType: 'application/pdf',
393
+ fileName: 'converted.pdf',
308
394
  },
309
- binary: {
310
- ...item.binary,
311
- [outputBinaryPropertyName]: {
312
- data: pdfBuffer.toString('base64'),
313
- mimeType: 'application/pdf',
314
- fileName: `converted_${itemIndex}.pdf`,
315
- },
395
+ },
396
+ };
397
+ returnData.push(newItem);
398
+ }
399
+ catch (error) {
400
+ if (this.continueOnFail()) {
401
+ // Если continueOnFail включен, возвращаем элемент с ошибкой
402
+ returnData.push({
403
+ json: {
404
+ ...aggregatedItem.json,
405
+ error: error instanceof Error ? error.message : String(error),
406
+ success: false,
316
407
  },
317
- };
318
- returnData.push(newItem);
408
+ binary: aggregatedItem.binary,
409
+ });
319
410
  }
320
- catch (error) {
321
- if (this.continueOnFail()) {
322
- returnData.push({
323
- json: {
324
- error: error instanceof Error ? error.message : String(error),
325
- success: false,
326
- },
327
- binary: items[itemIndex].binary,
328
- });
329
- continue;
330
- }
411
+ else {
331
412
  throw error;
332
413
  }
333
414
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-sotoros-gotenberg",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "n8n custom node for Gotenberg integration with binary data support",
5
5
  "keywords": [
6
6
  "n8n-community-node",