n8n-nodes-sotoros-gotenberg 1.0.2 → 1.0.5

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