first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import { readFileSync } from 'fs';
|
||||
import type { WorkflowTestData } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
|
||||
describe('Execute Spreadsheet File Node', () => {
|
||||
const testHarness = new NodeTestHarness();
|
||||
const readBinaryFile = (fileName: string) =>
|
||||
readFileSync(path.resolve(__dirname, fileName), 'base64');
|
||||
|
||||
const loadWorkflow = (fileName: string, csvName: string) => {
|
||||
const workflowData = testHarness.readWorkflowJSON(fileName);
|
||||
const node = workflowData.nodes.find((n) => n.name === 'Read Binary File')!;
|
||||
node.parameters.fileSelector = path.join(__dirname, csvName);
|
||||
return workflowData;
|
||||
};
|
||||
|
||||
const tests: WorkflowTestData[] = [
|
||||
{
|
||||
description: 'execute workflow.json',
|
||||
input: {
|
||||
workflowData: loadWorkflow('workflow.json', 'spreadsheet.csv'),
|
||||
},
|
||||
output: {
|
||||
assertBinaryData: true,
|
||||
nodeData: {
|
||||
'Read From File': [
|
||||
[
|
||||
{
|
||||
json: { A: 1, B: 2, C: 3 },
|
||||
},
|
||||
{
|
||||
json: { A: 4, B: 5, C: 6 },
|
||||
},
|
||||
],
|
||||
],
|
||||
'Read From File Range': [
|
||||
[
|
||||
{
|
||||
json: { '1': 4, '2': 5 },
|
||||
},
|
||||
],
|
||||
],
|
||||
'Read From File no Header Row': [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
row: ['A', 'B', 'C'],
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
row: [1, 2, 3],
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
row: [4, 5, 6],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
'Read From File Raw Data': [
|
||||
[
|
||||
{
|
||||
json: { A: '1', B: '2', C: '3' },
|
||||
},
|
||||
{
|
||||
json: { A: '4', B: '5', C: '6' },
|
||||
},
|
||||
],
|
||||
],
|
||||
'Read From File Read as String': [
|
||||
[
|
||||
{
|
||||
json: { A: 1, B: 2, C: 3 },
|
||||
},
|
||||
{
|
||||
json: { A: 4, B: 5, C: 6 },
|
||||
},
|
||||
],
|
||||
],
|
||||
'Read CSV with Row Limit': [[{ json: { A: '1', B: '2', C: '3' } }]],
|
||||
'Write To File CSV': [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: {
|
||||
mimeType: 'text/csv',
|
||||
fileType: 'text',
|
||||
fileExtension: 'csv',
|
||||
data: '77u/QSxCLEMKMSwyLDMKNCw1LDY=',
|
||||
fileName: 'spreadsheet.csv',
|
||||
fileSize: '20 B',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
'Write To File HTML': [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: {
|
||||
mimeType: 'text/html',
|
||||
fileType: 'html',
|
||||
fileExtension: 'html',
|
||||
data: readBinaryFile('spreadsheet.html'),
|
||||
fileName: 'spreadsheet.html',
|
||||
fileSize: '535 B',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
// ODS file has slight differences every time it's created
|
||||
//
|
||||
'Write To File RTF': [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: {
|
||||
mimeType: 'application/rtf',
|
||||
fileExtension: 'rtf',
|
||||
data: readBinaryFile('spreadsheet.rtf'),
|
||||
fileName: 'spreadsheet.rtf',
|
||||
fileSize: '267 B',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
'Write To File XLS': [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
binary: {
|
||||
data: {
|
||||
mimeType: 'application/vnd.ms-excel',
|
||||
fileExtension: 'xls',
|
||||
data: readBinaryFile('spreadsheet.xls'),
|
||||
fileName: 'spreadsheet.xls',
|
||||
fileSize: '3.58 kB',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'execute workflow.bom.json',
|
||||
input: {
|
||||
workflowData: loadWorkflow('workflow.bom.json', 'bom.csv'),
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Edit with BOM included': [[{ json: { X: null } }]],
|
||||
'Edit with BOM excluded': [[{ json: { X: '1' } }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'execute includeempty.json',
|
||||
input: {
|
||||
workflowData: loadWorkflow('workflow.empty.json', 'includeempty.csv'),
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Include Empty': [[{ json: { A: '1', B: '', C: '3' } }]],
|
||||
'Ignore Empty': [[{ json: { A: '1', C: '3' } }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'execute utf8.json',
|
||||
input: {
|
||||
workflowData: loadWorkflow('workflow.utf8.json', 'utf8.csv'),
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Parse UTF8 v1': [
|
||||
[{ json: { A: 1, B: '株式会社', C: 3 } }, { json: { A: 4, B: 5, C: '🐛' } }],
|
||||
],
|
||||
'Parse UTF8 v2': [
|
||||
[{ json: { A: '1', B: '株式会社', C: '3' } }, { json: { A: '4', B: '5', C: '🐛' } }],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testData of tests) {
|
||||
testHarness.setupTest(testData);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
a,b,c
|
||||
1,2,3
|
||||
|
@@ -0,0 +1,800 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IBinaryData, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow';
|
||||
import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
jest.mock('xlsx', () => ({
|
||||
read: jest.fn(),
|
||||
utils: {
|
||||
sheet_to_json: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { read as xlsxRead, utils as xlsxUtils } from 'xlsx';
|
||||
|
||||
import { execute } from '../v2/fromFile.operation';
|
||||
|
||||
describe('fromFile.operation - xlsx parsing logic', () => {
|
||||
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
|
||||
const mockBinaryDataInMemory: IBinaryData = {
|
||||
data: 'dGVzdCBkYXRh',
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
fileExtension: 'xlsx',
|
||||
fileName: 'test.xlsx',
|
||||
};
|
||||
|
||||
const mockBinaryDataWithId: IBinaryData = {
|
||||
id: 'binary-data-id-123',
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
fileExtension: 'xlsx',
|
||||
fileName: 'test.xlsx',
|
||||
data: '',
|
||||
};
|
||||
|
||||
const mockWorkbook = {
|
||||
SheetNames: ['Sheet1', 'Sheet2'],
|
||||
Sheets: {
|
||||
Sheet1: {
|
||||
A1: { t: 's', v: 'Name' },
|
||||
B1: { t: 's', v: 'Age' },
|
||||
C1: { t: 's', v: 'City' },
|
||||
A2: { t: 's', v: 'John' },
|
||||
B2: { t: 'n', v: 25 },
|
||||
C2: { t: 's', v: 'NYC' },
|
||||
A3: { t: 's', v: 'Jane' },
|
||||
B3: { t: 'n', v: 30 },
|
||||
C3: { t: 's', v: 'LA' },
|
||||
},
|
||||
Sheet2: {
|
||||
A1: { t: 's', v: 'Product' },
|
||||
B1: { t: 'n', v: 100 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockParsedData = [
|
||||
{ Name: 'John', Age: 25, City: 'NYC' },
|
||||
{ Name: 'Jane', Age: 30, City: 'LA' },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(paramName: string, _itemIndex: number, defaultValue?: any) => {
|
||||
switch (paramName) {
|
||||
case 'fileFormat':
|
||||
return 'xlsx';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataInMemory);
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
name: 'SpreadsheetFile',
|
||||
type: 'n8n-nodes-base.spreadsheetFile',
|
||||
id: 'test-node-id',
|
||||
} as INode);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbook);
|
||||
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockParsedData);
|
||||
});
|
||||
|
||||
describe('Basic xlsx parsing', () => {
|
||||
it('should parse xlsx file from in-memory binary data', async () => {
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].json).toEqual({ Name: 'John', Age: 25, City: 'NYC' });
|
||||
expect(result[1].json).toEqual({ Name: 'Jane', Age: 30, City: 'LA' });
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
expect(result[1].pairedItem).toEqual({ item: 0 });
|
||||
|
||||
expect(xlsxRead).toHaveBeenCalledWith(
|
||||
Buffer.from(mockBinaryDataInMemory.data, BINARY_ENCODING),
|
||||
{ raw: undefined },
|
||||
);
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {});
|
||||
});
|
||||
|
||||
it('should parse xlsx file from filesystem binary data', async () => {
|
||||
const mockStream = new Readable();
|
||||
mockStream.push(Buffer.from('test xlsx content'));
|
||||
mockStream.push(null);
|
||||
|
||||
const mockBuffer = Buffer.from('test xlsx content');
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataWithId);
|
||||
mockExecuteFunctions.helpers.getBinaryStream.mockResolvedValue(mockStream);
|
||||
mockExecuteFunctions.helpers.binaryToBuffer.mockResolvedValue(mockBuffer);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockExecuteFunctions.helpers.getBinaryStream).toHaveBeenCalledWith(
|
||||
'binary-data-id-123',
|
||||
262144,
|
||||
);
|
||||
expect(mockExecuteFunctions.helpers.binaryToBuffer).toHaveBeenCalledWith(mockStream);
|
||||
expect(xlsxRead).toHaveBeenCalledWith(mockBuffer, { raw: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options handling', () => {
|
||||
it('should respect rawData option', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { rawData: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(Buffer), { raw: true });
|
||||
});
|
||||
|
||||
it('should respect readAsString option', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
|
||||
});
|
||||
|
||||
it('should use specified sheet name', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { sheetName: 'Sheet2' };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet2, {});
|
||||
});
|
||||
|
||||
it('should handle range option as string', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { range: 'A1:B2' };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
|
||||
range: 'A1:B2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle range option as number', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { range: '2' };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
|
||||
range: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should include empty cells when option is set', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { includeEmptyCells: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
|
||||
defval: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle headerRow=false option', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { headerRow: false };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const mockArrayData = [
|
||||
['Name', 'Age'],
|
||||
['John', 25],
|
||||
['Jane', 30],
|
||||
];
|
||||
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockArrayData);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet1, {
|
||||
header: 1,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].json).toEqual({ row: ['Name', 'Age'] });
|
||||
expect(result[1].json).toEqual({ row: ['John', 25] });
|
||||
expect(result[2].json).toEqual({ row: ['Jane', 30] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should throw error when workbook has no sheets', async () => {
|
||||
const emptyWorkbook = { SheetNames: [], Sheets: {} };
|
||||
(xlsxRead as jest.Mock).mockReturnValue(emptyWorkbook);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await expect(execute.call(mockExecuteFunctions, items)).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should throw error when specified sheet does not exist', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { sheetName: 'NonExistentSheet' };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await expect(execute.call(mockExecuteFunctions, items)).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle continueOnFail gracefully', async () => {
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
(xlsxRead as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Invalid file format');
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].json.error).toContain('Invalid file format');
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
});
|
||||
|
||||
it('should enhance error message when file extension does not match format', async () => {
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue({
|
||||
...mockBinaryDataInMemory,
|
||||
fileExtension: 'pdf',
|
||||
});
|
||||
|
||||
(xlsxRead as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Parse error');
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result[0].json.error).toContain('not in xlsx format');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple items processing', () => {
|
||||
it('should process multiple items correctly', async () => {
|
||||
const items: INodeExecutionData[] = [{ json: { id: 1 } }, { json: { id: 2 } }];
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData
|
||||
.mockReturnValueOnce(mockBinaryDataInMemory)
|
||||
.mockReturnValueOnce({
|
||||
...mockBinaryDataInMemory,
|
||||
fileName: 'test2.xlsx',
|
||||
});
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
expect(result[1].pairedItem).toEqual({ item: 0 });
|
||||
expect(result[2].pairedItem).toEqual({ item: 1 });
|
||||
expect(result[3].pairedItem).toEqual({ item: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('File format detection', () => {
|
||||
it('should handle autodetect for xlsx files', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'fileFormat') return 'autodetect';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
if (paramName === 'options') return {};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(xlsxRead).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Additional edge cases', () => {
|
||||
it('should handle mixed file formats with autodetect', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'fileFormat') return 'autodetect';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
if (paramName === 'options') return {};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue({
|
||||
...mockBinaryDataInMemory,
|
||||
mimeType: 'application/octet-stream',
|
||||
fileExtension: 'xlsx',
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(xlsxRead).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle custom binary property name correctly', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'binaryPropertyName') return 'customBinaryField';
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'options') return {};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.assertBinaryData).toHaveBeenCalledWith(
|
||||
0,
|
||||
'customBinaryField',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle binary data stream errors gracefully', async () => {
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataWithId);
|
||||
mockExecuteFunctions.helpers.getBinaryStream.mockRejectedValue(new Error('Stream error'));
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].json.error).toContain('Stream error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Binary string conversion', () => {
|
||||
it('should convert buffer to binary string when readAsString is true', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
|
||||
const callArgs = (xlsxRead as jest.Mock).mock.calls[0];
|
||||
const passedData = callArgs[0];
|
||||
const expectedBinaryString = Buffer.from(
|
||||
mockBinaryDataInMemory.data,
|
||||
BINARY_ENCODING,
|
||||
).toString('binary');
|
||||
expect(passedData).toBe(expectedBinaryString);
|
||||
});
|
||||
|
||||
it('should use buffer directly when readAsString is false', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: false };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Verify that xlsxRead was called with a Buffer (no type specified)
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(Buffer), { raw: undefined });
|
||||
});
|
||||
|
||||
it('should handle readAsString with filesystem binary data', async () => {
|
||||
const mockStream = new Readable();
|
||||
mockStream.push(Buffer.from('test xlsx content'));
|
||||
mockStream.push(null);
|
||||
|
||||
const mockBuffer = Buffer.from('test xlsx content');
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataWithId);
|
||||
mockExecuteFunctions.helpers.getBinaryStream.mockResolvedValue(mockStream);
|
||||
mockExecuteFunctions.helpers.binaryToBuffer.mockResolvedValue(mockBuffer);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockExecuteFunctions.helpers.getBinaryStream).toHaveBeenCalledWith(
|
||||
'binary-data-id-123',
|
||||
262144,
|
||||
);
|
||||
expect(mockExecuteFunctions.helpers.binaryToBuffer).toHaveBeenCalledWith(mockStream);
|
||||
|
||||
// Verify that xlsxRead was called with binary string
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
|
||||
|
||||
// Verify the string is the result of buffer.toString('binary')
|
||||
const callArgs = (xlsxRead as jest.Mock).mock.calls[0];
|
||||
const passedData = callArgs[0];
|
||||
const expectedBinaryString = mockBuffer.toString('binary');
|
||||
expect(passedData).toBe(expectedBinaryString);
|
||||
});
|
||||
|
||||
it('should combine readAsString with other options correctly', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options')
|
||||
return { readAsString: true, rawData: true, sheetName: 'Sheet2' };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Verify that xlsxRead was called with binary string and rawData option
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: true, type: 'binary' });
|
||||
|
||||
// Verify that the correct sheet was used
|
||||
expect(xlsxUtils.sheet_to_json).toHaveBeenCalledWith(mockWorkbook.Sheets.Sheet2, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special character handling', () => {
|
||||
it('should handle special characters correctly when readAsString is true', async () => {
|
||||
// Mock binary data that contains special characters (e.g., accented characters, emojis)
|
||||
const specialCharBinaryData: IBinaryData = {
|
||||
data: Buffer.from('Special chars: àáâãäåæçèéêë 🚀 ñöü', 'utf8').toString(BINARY_ENCODING),
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
fileExtension: 'xlsx',
|
||||
fileName: 'special-chars.xlsx',
|
||||
};
|
||||
|
||||
const mockWorkbookWithSpecialChars = {
|
||||
SheetNames: ['Sheet1'],
|
||||
Sheets: {
|
||||
Sheet1: {
|
||||
A1: { t: 's', v: 'Special chars: àáâãäåæçèéêë 🚀 ñöü' },
|
||||
A2: { t: 's', v: 'Café' },
|
||||
A3: { t: 's', v: 'Naïve résumé' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockSpecialCharData = [
|
||||
{ text: 'Special chars: àáâãäåæçèéêë 🚀 ñöü' },
|
||||
{ text: 'Café' },
|
||||
{ text: 'Naïve résumé' },
|
||||
];
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(specialCharBinaryData);
|
||||
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbookWithSpecialChars);
|
||||
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockSpecialCharData);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Verify that xlsxRead was called with binary string type for proper character handling
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
|
||||
|
||||
// Verify that special characters are preserved in the output
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].json.text).toBe('Special chars: àáâãäåæçèéêë 🚀 ñöü');
|
||||
expect(result[1].json.text).toBe('Café');
|
||||
expect(result[2].json.text).toBe('Naïve résumé');
|
||||
});
|
||||
|
||||
it('should demonstrate the difference between readAsString true vs false for character encoding', async () => {
|
||||
// Test data with potential encoding issues
|
||||
const encodingTestData: IBinaryData = {
|
||||
data: Buffer.from('Encoding test: café naïve résumé', 'utf8').toString(BINARY_ENCODING),
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
fileExtension: 'xlsx',
|
||||
fileName: 'encoding-test.xlsx',
|
||||
};
|
||||
|
||||
const mockWorkbookEncoding = {
|
||||
SheetNames: ['Sheet1'],
|
||||
Sheets: {
|
||||
Sheet1: {
|
||||
A1: { t: 's', v: 'Encoding test: café naïve résumé' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(encodingTestData);
|
||||
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbookEncoding);
|
||||
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue([
|
||||
{ text: 'Encoding test: café naïve résumé' },
|
||||
]);
|
||||
|
||||
// Test with readAsString: true
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Verify that when readAsString is true, we use binary type for proper character handling
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
|
||||
|
||||
// Reset mocks for second test
|
||||
jest.clearAllMocks();
|
||||
(xlsxRead as jest.Mock).mockReturnValue(mockWorkbookEncoding);
|
||||
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue([
|
||||
{ text: 'Encoding test: café naïve résumé' },
|
||||
]);
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(encodingTestData);
|
||||
|
||||
// Test with readAsString: false (default)
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: false };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Verify that when readAsString is false, we use buffer directly (no type specified)
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(Buffer), { raw: undefined });
|
||||
});
|
||||
|
||||
it('should handle various international characters when readAsString is enabled', async () => {
|
||||
// Test with various international characters that might cause encoding issues
|
||||
const internationalChars = [
|
||||
'Chinese: 你好世界',
|
||||
'Japanese: こんにちは',
|
||||
'Korean: 안녕하세요',
|
||||
'Arabic: مرحبا',
|
||||
'Russian: Привет',
|
||||
'Greek: Γεια σας',
|
||||
'Hebrew: שלום',
|
||||
'Thai: สวัสดี',
|
||||
];
|
||||
|
||||
const internationalBinaryData: IBinaryData = {
|
||||
data: Buffer.from(internationalChars.join('\n'), 'utf8').toString(BINARY_ENCODING),
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
fileExtension: 'xlsx',
|
||||
fileName: 'international.xlsx',
|
||||
};
|
||||
|
||||
const mockInternationalWorkbook = {
|
||||
SheetNames: ['Sheet1'],
|
||||
Sheets: {
|
||||
Sheet1: internationalChars.reduce((acc, char, index) => {
|
||||
acc[`A${index + 1}`] = { t: 's', v: char };
|
||||
return acc;
|
||||
}, {} as any),
|
||||
},
|
||||
};
|
||||
|
||||
const mockInternationalData = internationalChars.map((char) => ({ text: char }));
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'options') return { readAsString: true };
|
||||
if (paramName === 'fileFormat') return 'xlsx';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(internationalBinaryData);
|
||||
(xlsxRead as jest.Mock).mockReturnValue(mockInternationalWorkbook);
|
||||
(xlsxUtils.sheet_to_json as jest.Mock).mockReturnValue(mockInternationalData);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Verify that xlsxRead was called with binary string type
|
||||
expect(xlsxRead).toHaveBeenCalledWith(expect.any(String), { raw: undefined, type: 'binary' });
|
||||
|
||||
// Verify that all international characters are preserved
|
||||
expect(result).toHaveLength(8);
|
||||
internationalChars.forEach((expectedChar, index) => {
|
||||
expect(result[index].json.text).toBe(expectedChar);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSV parsing with skipRecordsWithErrors', () => {
|
||||
const invalidCsvData = 'id,name\n3,"John"\n1,"Alice\n2,"Bob"';
|
||||
const mockBinaryDataCSV: IBinaryData = {
|
||||
data: Buffer.from(invalidCsvData, 'utf8').toString(BINARY_ENCODING),
|
||||
mimeType: 'text/csv',
|
||||
fileExtension: 'csv',
|
||||
fileName: 'test.csv',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(paramName: string, _itemIndex: number, defaultValue?: any) => {
|
||||
switch (paramName) {
|
||||
case 'fileFormat':
|
||||
return 'csv';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
case 'options':
|
||||
return {};
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
);
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataCSV);
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
name: 'SpreadsheetFile',
|
||||
type: 'n8n-nodes-base.spreadsheetFile',
|
||||
id: 'test-node-id',
|
||||
} as INode);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should skip records with errors when skipRecordsWithErrors is enabled with limit -1', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'fileFormat') return 'csv';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
if (paramName === 'options')
|
||||
return {
|
||||
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: -1 } },
|
||||
columns: true,
|
||||
};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
// Should have 1 valid record (John), Bob and Alice is considered a single record with error
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].json).toEqual({ id: '3', name: 'John' });
|
||||
});
|
||||
|
||||
it('should skip records with errors when skipRecordsWithErrors is enabled with limit 1', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'fileFormat') return 'csv';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
if (paramName === 'options')
|
||||
return {
|
||||
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: 1 } },
|
||||
columns: true,
|
||||
};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, items);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].json).toEqual({ id: '3', name: 'John' });
|
||||
expect(result[0].pairedItem).toEqual({ item: 0 });
|
||||
});
|
||||
|
||||
it('should throw error when skipped records exceed maxSkippedRecords limit', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'fileFormat') return 'csv';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
if (paramName === 'options')
|
||||
return {
|
||||
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: 1 } },
|
||||
columns: true,
|
||||
};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const csvWithThreeErrors =
|
||||
'id,name\n3,"John"\n1,"Alice\n2,"Bob"\n4,"Charlie\n5,"Eve\n6,"David';
|
||||
const mockBinaryDataThreeErrors: IBinaryData = {
|
||||
data: Buffer.from(csvWithThreeErrors, 'utf8').toString(BINARY_ENCODING),
|
||||
mimeType: 'text/csv',
|
||||
fileExtension: 'csv',
|
||||
fileName: 'test-three-errors.csv',
|
||||
};
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataThreeErrors);
|
||||
|
||||
const items: INodeExecutionData[] = [{ json: {} }];
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
if (paramName === 'fileFormat') return 'csv';
|
||||
if (paramName === 'binaryPropertyName') return 'data';
|
||||
if (paramName === 'options')
|
||||
return {
|
||||
skipRecordsWithErrors: { value: { enabled: true, maxSkippedRecords: 1 } },
|
||||
columns: true,
|
||||
};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
mockExecuteFunctions.helpers.assertBinaryData.mockReturnValue(mockBinaryDataThreeErrors);
|
||||
|
||||
await expect(execute.call(mockExecuteFunctions, items)).rejects.toThrow(
|
||||
'Max number of skipped records exceeded',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
A,B,C
|
||||
1,,3
|
||||
|
@@ -0,0 +1,3 @@
|
||||
A,B,C
|
||||
1,2,3
|
||||
4,5,6
|
||||
|
@@ -0,0 +1 @@
|
||||
<html><head><meta charset="utf-8"/><title>SheetJS Table Export</title></head><body><table><tr><td data-t="s" data-v="A" id="sjs-A1">A</td><td data-t="s" data-v="B" id="sjs-B1">B</td><td data-t="s" data-v="C" id="sjs-C1">C</td></tr><tr><td data-t="n" data-v="1" id="sjs-A2">1</td><td data-t="n" data-v="2" id="sjs-B2">2</td><td data-t="n" data-v="3" id="sjs-C2">3</td></tr><tr><td data-t="n" data-v="4" id="sjs-A3">4</td><td data-t="n" data-v="5" id="sjs-B3">5</td><td data-t="n" data-v="6" id="sjs-C3">6</td></tr></table></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
{\rtf1\ansi\trowd\trautofit1\cellx1\cellx2\cellx3\pard\intbl A\cell B\cell C\cell\pard\intbl\row\trowd\trautofit1\cellx1\cellx2\cellx3\pard\intbl 1\cell 2\cell 3\cell\pard\intbl\row\trowd\trautofit1\cellx1\cellx2\cellx3\pard\intbl 4\cell 5\cell 6\cell\pard\intbl\row}
|
||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
A,B,C
|
||||
1,株式会社,3
|
||||
4,5,🐛
|
||||
|
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "40bf604f-19f9-43e7-8bbb-74c36925f154",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-320,
|
||||
1040
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileSelector": "bom.csv"
|
||||
},
|
||||
"id": "623ea890-8882-4273-973e-834652d823b5",
|
||||
"name": "Read Binary File",
|
||||
"type": "n8n-nodes-base.readBinaryFiles",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-100,
|
||||
1040
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileFormat": "csv",
|
||||
"options": {
|
||||
"enableBOM": true
|
||||
}
|
||||
},
|
||||
"id": "c8cca5fb-e119-4ca1-a597-4f051a7f64ea",
|
||||
"name": "Exclude BOM",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
120,
|
||||
960
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileFormat": "csv",
|
||||
"options": {
|
||||
"enableBOM": false
|
||||
}
|
||||
},
|
||||
"id": "56ec11dc-966b-4d06-b8c0-61475b30333d",
|
||||
"name": "Include BOM",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
120,
|
||||
1180
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "X",
|
||||
"stringValue": "={{ $json.a }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "6f6bccf2-d674-4774-9df9-6f6fd893bace",
|
||||
"name": "Edit with BOM excluded",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
320,
|
||||
960
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "X",
|
||||
"stringValue": "={{ $json.a }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": "none",
|
||||
"options": {}
|
||||
},
|
||||
"id": "27ca5cde-19cb-4bf2-9ab4-7f7e77ad01bd",
|
||||
"name": "Edit with BOM included",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
320,
|
||||
1180
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read Binary File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Exclude BOM": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit with BOM excluded",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Include BOM": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit with BOM included",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read Binary File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Exclude BOM",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Include BOM",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"meta": {},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"fileFormat": "csv",
|
||||
"options": {
|
||||
"includeEmptyCells": false
|
||||
}
|
||||
},
|
||||
"id": "8aed098d-3c0b-43c9-b7e8-c4106c88b409",
|
||||
"name": "Ignore Empty",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1160,
|
||||
500
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "649db2c5-27dc-4cec-b084-8982632311e7",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
720,
|
||||
360
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileSelector": "includeempty.csv"
|
||||
},
|
||||
"id": "5056b8c4-fb6e-4ca1-9bc1-33f5db4027ad",
|
||||
"name": "Read Binary File",
|
||||
"type": "n8n-nodes-base.readBinaryFiles",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
940,
|
||||
360
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileFormat": "csv",
|
||||
"options": {
|
||||
"includeEmptyCells": true
|
||||
}
|
||||
},
|
||||
"id": "a4822e75-d638-45c8-887f-0487d5237267",
|
||||
"name": "Include Empty",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1160,
|
||||
280
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read Binary File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read Binary File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Include Empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Ignore Empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "087277cc-297d-4912-bd11-86626eff2d71",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
620,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "f55bc21c-c9a8-43af-bbc8-e4bdd30f0ce9",
|
||||
"name": "Read From File",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1260,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileSelector": "spreadsheet.csv"
|
||||
},
|
||||
"id": "d7620053-eb3d-43dd-b2cd-d60d9a08a9cc",
|
||||
"name": "Read Binary File",
|
||||
"type": "n8n-nodes-base.readBinaryFiles",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
840,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toFile",
|
||||
"fileFormat": "csv",
|
||||
"options": {}
|
||||
},
|
||||
"id": "21bc49fe-1e6b-46d6-a04d-cb474d138e02",
|
||||
"name": "Write To File CSV",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1580,
|
||||
280
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toFile",
|
||||
"fileFormat": "html",
|
||||
"options": {}
|
||||
},
|
||||
"id": "a4c2c717-5a9d-4fd6-8450-6bb78e233c05",
|
||||
"name": "Write To File HTML",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1580,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toFile",
|
||||
"fileFormat": "ods",
|
||||
"options": {}
|
||||
},
|
||||
"id": "58e5a423-0477-44df-a505-6b8a40dbf275",
|
||||
"name": "Write To File ODS",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1580,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toFile",
|
||||
"fileFormat": "rtf",
|
||||
"options": {}
|
||||
},
|
||||
"id": "3ae6e9c5-bc0a-44e4-959e-cfc572f4179f",
|
||||
"name": "Write To File RTF",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1580,
|
||||
820
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "toFile",
|
||||
"options": {}
|
||||
},
|
||||
"id": "7e6db847-d24c-4094-907d-92ffec626f68",
|
||||
"name": "Write To File XLS",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1580,
|
||||
1020
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {
|
||||
"range": "A2:B3"
|
||||
}
|
||||
},
|
||||
"id": "48934f0d-ac10-4862-ae0c-2ea591b111e3",
|
||||
"name": "Read From File Range",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1060,
|
||||
520
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {
|
||||
"headerRow": false
|
||||
}
|
||||
},
|
||||
"id": "dea6f3f6-f2fb-472e-97b2-8a0c0a36bc4d",
|
||||
"name": "Read From File no Header Row",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1060,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {
|
||||
"rawData": true
|
||||
}
|
||||
},
|
||||
"id": "38ed33fc-6906-4b09-8376-937ef3ca99be",
|
||||
"name": "Read From File Raw Data",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1060,
|
||||
740
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {
|
||||
"readAsString": true
|
||||
}
|
||||
},
|
||||
"id": "ffe09dc8-9b7a-4baf-bd03-bd65bbce2590",
|
||||
"name": "Read From File Read as String",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1060,
|
||||
940
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileFormat": "csv",
|
||||
"options": {
|
||||
"maxRowCount": 1
|
||||
}
|
||||
},
|
||||
"id": "de905389-a11b-4dd8-8416-14d650804445",
|
||||
"name": "Read CSV with Row Limit",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
-60,
|
||||
1340
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read Binary File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read From File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Write To File CSV",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Write To File HTML",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Write To File ODS",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Write To File RTF",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Write To File XLS",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read Binary File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read From File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Read From File Range",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Read From File no Header Row",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Read From File Raw Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Read From File Read as String",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Read CSV with Row Limit",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"meta": {
|
||||
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "2130ab19-2efb-4217-b234-f8607d4122cc",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
260,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {
|
||||
"readAsString": true
|
||||
}
|
||||
},
|
||||
"id": "68e03042-aa27-43db-bfec-3c4fe07ce9f6",
|
||||
"name": "Parse UTF8 v1",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
760,
|
||||
360
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileFormat": "csv",
|
||||
"options": {
|
||||
"readAsString": true
|
||||
}
|
||||
},
|
||||
"id": "6a8b7ee9-5d14-4b67-b7cc-afee6bcc1fa6",
|
||||
"name": "Parse UTF8 v2",
|
||||
"type": "n8n-nodes-base.spreadsheetFile",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
760,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileSelector": "utf8.csv"
|
||||
},
|
||||
"id": "623ea890-8882-4273-973e-834652d823b5",
|
||||
"name": "Read Binary File",
|
||||
"type": "n8n-nodes-base.readBinaryFiles",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
480,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read Binary File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read Binary File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse UTF8 v1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Parse UTF8 v2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user