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
1392 lines
36 KiB
TypeScript
1392 lines
36 KiB
TypeScript
import type { IDataObject, INode } from 'n8n-workflow';
|
|
|
|
import { createMockExecuteFunction } from '@test/nodes/Helpers';
|
|
|
|
import * as mode from '../../v3/actions/mode';
|
|
|
|
const node: INode = {
|
|
id: '123456',
|
|
name: 'Merge',
|
|
typeVersion: 3,
|
|
type: 'n8n-nodes-base.merge',
|
|
position: [50, 50],
|
|
parameters: {},
|
|
};
|
|
|
|
const inputsData = [
|
|
[
|
|
{
|
|
json: {
|
|
id: 1,
|
|
data: 'a',
|
|
name: 'Sam',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 2,
|
|
data: 'b',
|
|
name: 'Dan',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 3,
|
|
data: 'c',
|
|
name: 'Jon',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 6,
|
|
data: 'e',
|
|
name: 'Ron',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 7,
|
|
data: 'f',
|
|
name: 'Joe',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
json: {
|
|
id: 1,
|
|
data: 'aa',
|
|
country: 'PL',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 2,
|
|
data: 'bb',
|
|
country: 'FR',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 3,
|
|
data: 'cc',
|
|
country: 'UA',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 4,
|
|
data: 'ee',
|
|
country: 'US',
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 5,
|
|
data: 'ff',
|
|
country: 'ES',
|
|
},
|
|
},
|
|
],
|
|
];
|
|
describe('Test MergeV3, combineBySql operation', () => {
|
|
it('LEFT JOIN', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query:
|
|
'SELECT *, input1.data as data_1\nFROM input1\nLEFT JOIN input2\nON input1.id = input2.id\n',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData[0][0].json).toEqual({
|
|
data_1: 'a',
|
|
id: 1,
|
|
data: 'aa',
|
|
name: 'Sam',
|
|
country: 'PL',
|
|
});
|
|
});
|
|
it('LEFT JOIN, missing input 2(empty array)', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query:
|
|
'SELECT *, input1.data as data_1\nFROM input1\nLEFT JOIN input2\nON input1.id = input2.id\n',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
[inputsData[0], []],
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(5);
|
|
expect(returnData[0][0].json).toEqual({
|
|
data: 'a',
|
|
data_1: 'a',
|
|
id: 1,
|
|
name: 'Sam',
|
|
});
|
|
});
|
|
|
|
it('LEFT JOIN, missing data in input 2', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query:
|
|
'SELECT *, input1.data as data_1\nFROM input1\nLEFT JOIN input2\nON input1.id = input2.id\n',
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), [
|
|
inputsData[0],
|
|
]),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
description: 'Table does not exist: input2',
|
|
});
|
|
});
|
|
|
|
it('LEFT JOIN, invalid syntax', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query:
|
|
'SELECTTT *, input1.data as data_1\nFROM input1\nLEFT JOIN input2\nON input1.id = input2.id\n',
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
description: expect.stringContaining('Parse error'),
|
|
});
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
description: expect.stringContaining('SELECTTT'),
|
|
});
|
|
});
|
|
|
|
it('RIGHT JOIN', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT *\nFROM input1\nRIGHT JOIN input2\nON input1.id = input2.id;\n',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(5);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 1,
|
|
data: 'aa',
|
|
name: 'Sam',
|
|
country: 'PL',
|
|
});
|
|
expect(returnData[0][4].json).toEqual({
|
|
id: 5,
|
|
data: 'ff',
|
|
country: 'ES',
|
|
});
|
|
});
|
|
|
|
it('INNER JOIN', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT *\nFROM input1\nINNER JOIN input2\nON input1.id = input2.id;\n',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(3);
|
|
expect(returnData[0][2].json).toEqual({
|
|
id: 3,
|
|
data: 'cc',
|
|
name: 'Jon',
|
|
country: 'UA',
|
|
});
|
|
});
|
|
|
|
it('FULL OUTER JOIN', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT *\nFROM input1\nFULL OUTER JOIN input2\nON input1.id = input2.id;\n',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(7);
|
|
expect(returnData[0][2].json).toEqual({
|
|
id: 3,
|
|
data: 'cc',
|
|
name: 'Jon',
|
|
country: 'UA',
|
|
});
|
|
});
|
|
it('CROSS JOIN', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT *, input1.data AS data_1\nFROM input1\nCROSS JOIN input2;\n',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(25);
|
|
expect(returnData[0][0].json).toEqual({
|
|
data_1: 'a',
|
|
id: 1,
|
|
data: 'aa',
|
|
name: 'Sam',
|
|
country: 'PL',
|
|
});
|
|
});
|
|
|
|
it('should collect pairedItems data, if version >= 3.1 and SELECT query', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT name FROM input1 LEFT JOIN input2 ON input1.id = input2.id',
|
|
};
|
|
|
|
const inputs = [
|
|
[
|
|
{
|
|
json: {
|
|
id: 1,
|
|
data: 'a',
|
|
name: 'Sam',
|
|
},
|
|
pairedItem: {
|
|
item: 0,
|
|
input: undefined,
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 2,
|
|
data: 'b',
|
|
name: 'Dan',
|
|
},
|
|
pairedItem: {
|
|
item: 1,
|
|
input: undefined,
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 3,
|
|
data: 'c',
|
|
name: 'Jon',
|
|
},
|
|
pairedItem: {
|
|
item: 2,
|
|
input: undefined,
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
json: {
|
|
id: 1,
|
|
data: 'aa',
|
|
country: 'PL',
|
|
},
|
|
pairedItem: {
|
|
item: 0,
|
|
input: 1,
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 2,
|
|
data: 'bb',
|
|
country: 'FR',
|
|
},
|
|
pairedItem: {
|
|
item: 1,
|
|
input: 1,
|
|
},
|
|
},
|
|
{
|
|
json: {
|
|
id: 3,
|
|
data: 'cc',
|
|
country: 'UA',
|
|
},
|
|
pairedItem: {
|
|
item: 2,
|
|
input: 1,
|
|
},
|
|
},
|
|
],
|
|
];
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, { ...node, typeVersion: 3.1 }),
|
|
inputs,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData).toEqual([
|
|
[
|
|
{
|
|
json: {
|
|
name: 'Sam',
|
|
},
|
|
pairedItem: [
|
|
{
|
|
item: 0,
|
|
input: undefined,
|
|
},
|
|
{
|
|
item: 0,
|
|
input: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
json: {
|
|
name: 'Dan',
|
|
},
|
|
pairedItem: [
|
|
{
|
|
item: 1,
|
|
input: undefined,
|
|
},
|
|
{
|
|
item: 1,
|
|
input: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
json: {
|
|
name: 'Jon',
|
|
},
|
|
pairedItem: [
|
|
{
|
|
item: 2,
|
|
input: undefined,
|
|
},
|
|
{
|
|
item: 2,
|
|
input: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('Empty successful query should return [{ success: true }] at version <= 3.1', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT id from input1',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, { ...node, typeVersion: 3.1 }),
|
|
[[]],
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(1);
|
|
expect(returnData[0][0].json).toEqual({
|
|
success: true,
|
|
});
|
|
});
|
|
|
|
it('Empty successful query should return [] at version >= 3.2 if no option set', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT id from input1',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, { ...node, typeVersion: 3.2 }),
|
|
[[]],
|
|
);
|
|
|
|
expect(returnData).toEqual([[]]);
|
|
});
|
|
|
|
it('Empty successful query should return [{ success: true }] at version >= 3.2 if option set', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT id from input1',
|
|
options: {
|
|
emptyQueryResult: 'success',
|
|
},
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, { ...node, typeVersion: 3.2 }),
|
|
[[]],
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(1);
|
|
expect(returnData[0][0].json).toEqual({
|
|
success: true,
|
|
});
|
|
});
|
|
|
|
describe('Security Tests', () => {
|
|
it('should block FILE() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM FILE('test.csv')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block JSON() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM JSON('data.json')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block TXT() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM TXT('data.txt')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block CSV() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM CSV('data.csv')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block XLSX() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM XLSX('data.xlsx')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block XLS() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM XLS('data.xls')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block JSONL() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM JSONL('data.jsonl')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block NDJSON() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM NDJSON('data.ndjson')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block TAB() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM TAB('data.tab')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block TSV() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM TSV('data.tsv')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block ODS() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM ODS('data.ods')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block XML() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM XML('data.xml')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO TAB() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO TAB('output.tab') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO TSV() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO TSV('output.tsv') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO SQL() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO SQL('output.sql') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO XLSXML() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO XLSXML('output.xml') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO HTML() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO HTML('output.html') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block LOAD() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM input1 WHERE id = LOAD('test.txt')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block SAVE() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT id, SAVE('output.txt', name) as saved FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO FILE() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO FILE('output.txt') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO JSON() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO JSON('output.json') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO CSV() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO CSV('output.csv', {headers: true}) FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO TXT() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO TXT('output.txt') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO XLSX() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO XLSX('output.xlsx') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block INTO XLS() write operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * INTO XLS('output.xls') FROM input1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block GEXF() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM GEXF('graph.gexf')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block HTML() FROM function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM HTML('#table-id')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block TABLETOP() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM TABLETOP('spreadsheet-key')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block METEOR() function access', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "SELECT * FROM METEOR('collection')",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should still allow normal SQL operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT id, name FROM input1 WHERE id > 1',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(4);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 2,
|
|
name: 'Dan',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Database Engine Security Tests', () => {
|
|
it('should block CREATE FILESTORAGE DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "CREATE FILESTORAGE DATABASE 'test.json'",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block ATTACH FILESTORAGE DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "ATTACH FILESTORAGE DATABASE 'data.json' AS fs1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block CREATE LOCALSTORAGE DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "CREATE LOCALSTORAGE DATABASE 'localdb'",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block ATTACH LOCALSTORAGE DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "ATTACH LOCALSTORAGE DATABASE 'localdb' AS ls1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block CREATE INDEXEDDB DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "CREATE INDEXEDDB DATABASE 'indexeddb'",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block ATTACH INDEXEDDB DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "ATTACH INDEXEDDB DATABASE 'indexeddb' AS idb1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block ATTACH SQLITE DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "ATTACH SQLITE DATABASE 'app.db' AS sqlite1",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
|
|
it('should block CREATE FILE DATABASE', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: "CREATE FILE DATABASE 'file.db'",
|
|
};
|
|
|
|
await expect(
|
|
mode.combineBySql.execute.call(createMockExecuteFunction(nodeParameters, node), inputsData),
|
|
).rejects.toMatchObject({
|
|
message: 'Issue while executing query',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Advanced SQL Query Tests', () => {
|
|
it('should handle UNION queries', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT id, name FROM input1 UNION SELECT id, country as name FROM input2',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(10);
|
|
// Should contain both names from input1 and countries from input2
|
|
expect(returnData[0].some((item) => item.json.name === 'Sam')).toBe(true);
|
|
expect(returnData[0].some((item) => item.json.name === 'PL')).toBe(true);
|
|
});
|
|
|
|
it('should handle UNION ALL queries', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT id FROM input1 UNION ALL SELECT id FROM input2',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(10);
|
|
// Should include duplicate IDs
|
|
const ids = returnData[0].map((item) => item.json.id);
|
|
expect(ids.filter((id) => id === 1).length).toBe(2);
|
|
});
|
|
|
|
it('should handle subqueries', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT * FROM input1 WHERE id IN (SELECT id FROM input2 WHERE country = "PL")',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(1);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 1,
|
|
data: 'a',
|
|
name: 'Sam',
|
|
});
|
|
});
|
|
|
|
it('should handle basic filtering', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT * FROM input1 WHERE id < 4',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(3);
|
|
expect(returnData[0][0].json.id).toBe(1);
|
|
expect(returnData[0][1].json.id).toBe(2);
|
|
expect(returnData[0][2].json.id).toBe(3);
|
|
});
|
|
|
|
it('should handle CASE statements', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT id, name,
|
|
CASE
|
|
WHEN id <= 2 THEN 'Low'
|
|
WHEN id <= 5 THEN 'Medium'
|
|
ELSE 'High'
|
|
END as category
|
|
FROM input1`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(5);
|
|
expect(returnData[0][0].json.category).toBe('Low');
|
|
expect(returnData[0][2].json.category).toBe('Medium');
|
|
expect(returnData[0][4].json.category).toBe('High');
|
|
});
|
|
|
|
it('should handle ORDER BY with multiple columns', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT * FROM input1 ORDER BY name DESC, id ASC',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(5);
|
|
// Should be ordered by name DESC, then id ASC
|
|
expect(returnData[0][0].json.name).toBe('Sam');
|
|
expect(returnData[0][4].json.name).toBe('Dan');
|
|
});
|
|
|
|
it('should handle LIMIT and OFFSET', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT * FROM input1 ORDER BY id LIMIT 2 OFFSET 1',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(2);
|
|
expect(returnData[0][0].json.id).toBe(2);
|
|
expect(returnData[0][1].json.id).toBe(3);
|
|
});
|
|
|
|
it('should handle string functions', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT
|
|
UPPER(name) as upper_name,
|
|
LOWER(name) as lower_name,
|
|
LENGTH(name) as name_length,
|
|
CONCAT(name, '_', data) as combined
|
|
FROM input1 WHERE id = 1`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(1);
|
|
expect(returnData[0][0].json).toEqual({
|
|
upper_name: 'SAM',
|
|
lower_name: 'sam',
|
|
name_length: 3,
|
|
combined: 'Sam_a',
|
|
});
|
|
});
|
|
|
|
it('should handle mathematical operations', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT
|
|
id,
|
|
id * 2 as doubled,
|
|
id + 10 as plus_ten,
|
|
id % 2 as modulo,
|
|
ROUND(id / 3.0, 2) as divided
|
|
FROM input1 WHERE id <= 3`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(3);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 1,
|
|
doubled: 2,
|
|
plus_ten: 11,
|
|
modulo: 1,
|
|
divided: 0.33,
|
|
});
|
|
});
|
|
|
|
it('should handle simple WHERE clauses', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: 'SELECT * FROM input1 WHERE name LIKE "%o%"',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(3);
|
|
// Should contain Jon, Ron, and Joe (all names with 'o')
|
|
expect(returnData[0].some((item) => item.json.name === 'Jon')).toBe(true);
|
|
expect(returnData[0].some((item) => item.json.name === 'Ron')).toBe(true);
|
|
expect(returnData[0].some((item) => item.json.name === 'Joe')).toBe(true);
|
|
});
|
|
|
|
it('should handle complex JOIN with WHERE conditions', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT
|
|
input1.name,
|
|
input2.country,
|
|
input1.data as input1_data,
|
|
input2.data as input2_data
|
|
FROM input1
|
|
INNER JOIN input2 ON input1.id = input2.id
|
|
WHERE input1.id > 1 AND input2.country IN ('FR', 'UA')
|
|
ORDER BY input1.id`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(2);
|
|
expect(returnData[0][0].json).toEqual({
|
|
name: 'Dan',
|
|
country: 'FR',
|
|
input1_data: 'b',
|
|
input2_data: 'bb',
|
|
});
|
|
expect(returnData[0][1].json).toEqual({
|
|
name: 'Jon',
|
|
country: 'UA',
|
|
input1_data: 'c',
|
|
input2_data: 'cc',
|
|
});
|
|
});
|
|
|
|
it('should handle EXISTS subquery', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT * FROM input1
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM input2
|
|
WHERE input2.id = input1.id AND input2.country = 'PL'
|
|
)`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(1);
|
|
expect(returnData[0][0].json.name).toBe('Sam');
|
|
});
|
|
|
|
it('should handle NOT EXISTS subquery', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT * FROM input1
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM input2
|
|
WHERE input2.id = input1.id
|
|
)
|
|
ORDER BY id`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(2);
|
|
// Should return records from input1 that don't have matching IDs in input2
|
|
expect(returnData[0][0].json.id).toBe(6);
|
|
expect(returnData[0][1].json.id).toBe(7);
|
|
});
|
|
|
|
it('should handle DISTINCT queries', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query:
|
|
'SELECT DISTINCT SUBSTR(name, 1, 1) as first_letter FROM input1 ORDER BY first_letter',
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
// Should return unique first letters only
|
|
const letters = returnData[0].map((item) => item.json.first_letter);
|
|
expect(letters).toEqual([...new Set(letters)].sort());
|
|
});
|
|
|
|
it('should handle multiple table operations with aliases', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
operation: 'combineBySql',
|
|
query: `SELECT
|
|
u.name as user_name,
|
|
p.country as user_country,
|
|
u.id
|
|
FROM input1 u
|
|
LEFT JOIN input2 p ON u.id = p.id
|
|
WHERE u.name LIKE '%an%'`,
|
|
};
|
|
|
|
const returnData = await mode.combineBySql.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(1);
|
|
expect(returnData[0][0].json).toEqual({
|
|
user_name: 'Dan',
|
|
user_country: 'FR',
|
|
id: 2,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test MergeV3, append operation', () => {
|
|
it('append inputs', async () => {
|
|
const nodeParameters: IDataObject = {};
|
|
|
|
const returnData = await mode.append.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(10);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 1,
|
|
data: 'a',
|
|
name: 'Sam',
|
|
});
|
|
});
|
|
});
|
|
describe('Test MergeV3, combineByFields operation', () => {
|
|
it('merge inputs', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
joinMode: 'keepMatches',
|
|
fieldsToMatchString: 'id',
|
|
options: {},
|
|
};
|
|
|
|
const returnData = await mode.combineByFields.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(3);
|
|
expect(returnData[0][1].json).toEqual({
|
|
id: 2,
|
|
data: 'bb',
|
|
name: 'Dan',
|
|
country: 'FR',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test MergeV3, combineByPosition operation', () => {
|
|
it('combine inputs', async () => {
|
|
const nodeParameters: IDataObject = {};
|
|
|
|
const returnData = await mode.combineByPosition.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(5);
|
|
expect(returnData[0][4].json).toEqual({
|
|
id: 5,
|
|
data: 'ff',
|
|
name: 'Joe',
|
|
country: 'ES',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test MergeV3, chooseBranch operation', () => {
|
|
it('choose input', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
useDataOfInput: 2,
|
|
chooseBranchMode: 'waitForAll',
|
|
output: 'specifiedInput',
|
|
};
|
|
|
|
const returnData = await mode.chooseBranch.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(5);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 1,
|
|
data: 'aa',
|
|
country: 'PL',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Test MergeV3, combineAll operation', () => {
|
|
it('combine inputs', async () => {
|
|
const nodeParameters: IDataObject = {
|
|
options: {},
|
|
};
|
|
|
|
const returnData = await mode.combineAll.execute.call(
|
|
createMockExecuteFunction(nodeParameters, node),
|
|
inputsData,
|
|
);
|
|
|
|
expect(returnData.length).toEqual(1);
|
|
expect(returnData[0].length).toEqual(25);
|
|
expect(returnData[0][0].json).toEqual({
|
|
id: 1,
|
|
data: 'aa',
|
|
name: 'Sam',
|
|
country: 'PL',
|
|
});
|
|
});
|
|
});
|