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,19 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.sort",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.sort/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
},
|
||||
"alias": ["Sort", "Order", "Transform", "Array", "List", "Item", "Random"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Data Transformation"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import get from 'lodash/get';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import lt from 'lodash/lt';
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { shuffleArray } from '@utils/utilities';
|
||||
|
||||
import { sortByCode } from './utils';
|
||||
|
||||
export class Sort implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Sort',
|
||||
name: 'sort',
|
||||
icon: 'file:sort.svg',
|
||||
group: ['transform'],
|
||||
subtitle: '',
|
||||
version: 1,
|
||||
description: 'Change items order',
|
||||
defaults: {
|
||||
name: 'Sort',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Simple',
|
||||
value: 'simple',
|
||||
},
|
||||
{
|
||||
name: 'Random',
|
||||
value: 'random',
|
||||
},
|
||||
{
|
||||
name: 'Code',
|
||||
value: 'code',
|
||||
},
|
||||
],
|
||||
default: 'simple',
|
||||
description: 'The type of sorting to perform',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields To Sort By',
|
||||
name: 'sortFieldsUi',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Field To Sort By',
|
||||
options: [
|
||||
{
|
||||
displayName: '',
|
||||
name: 'sortField',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Name',
|
||||
name: 'fieldName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The field to sort by',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
|
||||
placeholder: 'e.g. id',
|
||||
hint: ' Enter the field name as text',
|
||||
requiresDataPath: 'single',
|
||||
},
|
||||
{
|
||||
displayName: 'Order',
|
||||
name: 'order',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Ascending',
|
||||
value: 'ascending',
|
||||
},
|
||||
{
|
||||
name: 'Descending',
|
||||
value: 'descending',
|
||||
},
|
||||
],
|
||||
default: 'ascending',
|
||||
description: 'The order to sort by',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
default: {},
|
||||
description: 'The fields of the input items to sort by',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['simple'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Code',
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
editor: 'jsEditor',
|
||||
rows: 10,
|
||||
},
|
||||
default: `// The two items to compare are in the variables a and b
|
||||
// Access the fields in a.json and b.json
|
||||
// Return -1 if a should go before b
|
||||
// Return 1 if b should go before a
|
||||
// Return 0 if there's no difference
|
||||
|
||||
fieldName = 'myField';
|
||||
|
||||
if (a.json[fieldName] < b.json[fieldName]) {
|
||||
return -1;
|
||||
}
|
||||
if (a.json[fieldName] > b.json[fieldName]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;`,
|
||||
description: 'Javascript code to determine the order of any two items',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['code'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['simple'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Disable Dot Notation',
|
||||
name: 'disableDotNotation',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to disallow referencing child fields using `parent.child` in the field name',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
let returnData = [...items];
|
||||
const type = this.getNodeParameter('type', 0) as string;
|
||||
const disableDotNotation = this.getNodeParameter(
|
||||
'options.disableDotNotation',
|
||||
0,
|
||||
false,
|
||||
) as boolean;
|
||||
|
||||
if (type === 'random') {
|
||||
shuffleArray(returnData);
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
if (type === 'simple') {
|
||||
const sortFieldsUi = this.getNodeParameter('sortFieldsUi', 0) as IDataObject;
|
||||
const sortFields = sortFieldsUi.sortField as Array<{
|
||||
fieldName: string;
|
||||
order: 'ascending' | 'descending';
|
||||
}>;
|
||||
|
||||
if (!sortFields?.length) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'No sorting specified. Please add a field to sort by',
|
||||
);
|
||||
}
|
||||
|
||||
for (const { fieldName } of sortFields) {
|
||||
let found = false;
|
||||
for (const item of items) {
|
||||
if (!disableDotNotation) {
|
||||
if (get(item.json, fieldName) !== undefined) {
|
||||
found = true;
|
||||
}
|
||||
} else if (item.json.hasOwnProperty(fieldName)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found && disableDotNotation && fieldName.includes('.')) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Couldn't find the field '${fieldName}' in the input data`,
|
||||
{
|
||||
description:
|
||||
"If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options",
|
||||
},
|
||||
);
|
||||
} else if (!found) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Couldn't find the field '${fieldName}' in the input data`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortFieldsWithDirection = sortFields.map((field) => ({
|
||||
name: field.fieldName,
|
||||
dir: field.order === 'ascending' ? 1 : -1,
|
||||
}));
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
let result = 0;
|
||||
for (const field of sortFieldsWithDirection) {
|
||||
let equal;
|
||||
if (!disableDotNotation) {
|
||||
const _a =
|
||||
typeof get(a.json, field.name) === 'string'
|
||||
? (get(a.json, field.name) as string).toLowerCase()
|
||||
: get(a.json, field.name);
|
||||
const _b =
|
||||
typeof get(b.json, field.name) === 'string'
|
||||
? (get(b.json, field.name) as string).toLowerCase()
|
||||
: get(b.json, field.name);
|
||||
equal = isEqual(_a, _b);
|
||||
} else {
|
||||
const _a =
|
||||
typeof a.json[field.name] === 'string'
|
||||
? (a.json[field.name] as string).toLowerCase()
|
||||
: a.json[field.name];
|
||||
const _b =
|
||||
typeof b.json[field.name] === 'string'
|
||||
? (b.json[field.name] as string).toLowerCase()
|
||||
: b.json[field.name];
|
||||
equal = isEqual(_a, _b);
|
||||
}
|
||||
|
||||
if (!equal) {
|
||||
let lessThan;
|
||||
if (!disableDotNotation) {
|
||||
const _a =
|
||||
typeof get(a.json, field.name) === 'string'
|
||||
? (get(a.json, field.name) as string).toLowerCase()
|
||||
: get(a.json, field.name);
|
||||
const _b =
|
||||
typeof get(b.json, field.name) === 'string'
|
||||
? (get(b.json, field.name) as string).toLowerCase()
|
||||
: get(b.json, field.name);
|
||||
lessThan = lt(_a, _b);
|
||||
} else {
|
||||
const _a =
|
||||
typeof a.json[field.name] === 'string'
|
||||
? (a.json[field.name] as string).toLowerCase()
|
||||
: a.json[field.name];
|
||||
const _b =
|
||||
typeof b.json[field.name] === 'string'
|
||||
? (b.json[field.name] as string).toLowerCase()
|
||||
: b.json[field.name];
|
||||
lessThan = lt(_a, _b);
|
||||
}
|
||||
if (lessThan) {
|
||||
result = -1 * field.dir;
|
||||
} else {
|
||||
result = 1 * field.dir;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
returnData = await sortByCode.call(this);
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#8287EB" fill-rule="evenodd" d="M130.5 422.5c-6.627 0-12-5.373-12-12v-351c0-6.627 5.373-12 12-12h24c6.627 0 12 5.373 12 12v351c0 6.627-5.373 12-12 12z" clip-rule="evenodd"/><path fill="#8287EB" fill-rule="evenodd" d="M36.077 333.482c9.398-9.346 24.594-9.304 33.94.095l72.483 72.887 72.482-72.887c9.347-9.399 24.543-9.441 33.941-.095s9.441 24.543.095 33.941l-89.5 90a24 24 0 0 1-34.036 0l-89.5-90c-9.346-9.398-9.304-24.594.095-33.941M381.5 89.5c6.627 0 12 5.373 12 12v351c0 6.627-5.373 12-12 12h-24c-6.627 0-12-5.373-12-12v-351c0-6.627 5.373-12 12-12z" clip-rule="evenodd"/><path fill="#8287EB" fill-rule="evenodd" d="M475.923 178.518c-9.398 9.346-24.594 9.304-33.941-.095L369.5 105.536l-72.482 72.887c-9.347 9.399-24.543 9.441-33.941.095s-9.441-24.543-.095-33.941l89.5-90a24 24 0 0 1 34.036 0l89.5 90c9.346 9.398 9.304 24.594-.095 33.941" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 955 B |
@@ -0,0 +1,25 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { JsTaskRunnerSandbox } from '../../Code/JsTaskRunnerSandbox';
|
||||
|
||||
const returnRegExp = /\breturn\b/;
|
||||
|
||||
export async function sortByCode(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
|
||||
const userCode = this.getNodeParameter('code', 0) as string;
|
||||
if (!returnRegExp.test(userCode)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
"Sort code doesn't return. Please add a 'return' statement to your code",
|
||||
);
|
||||
}
|
||||
|
||||
const mode = this.getMode();
|
||||
const items = this.getInputData();
|
||||
const code = `return items.sort((a, b) => { ${userCode} })`;
|
||||
const chunkSize = undefined;
|
||||
const sandbox = new JsTaskRunnerSandbox(mode, this, chunkSize, { items });
|
||||
const executionResult = await sandbox.runCode<INodeExecutionData[]>(code);
|
||||
|
||||
return executionResult;
|
||||
}
|
||||
Reference in New Issue
Block a user