Skip to content

Commit

Permalink
feat: Add new @n8n/utils package (no-changelog) (#13536)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgrozav authored Feb 27, 2025
1 parent ebaaf0e commit 7fb88e6
Show file tree
Hide file tree
Showing 125 changed files with 379 additions and 202 deletions.
10 changes: 10 additions & 0 deletions packages/@n8n/utils/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const sharedOptions = require('@n8n/eslint-config/shared');

/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['@n8n/eslint-config/node'],

...sharedOptions(__dirname),
};
24 changes: 24 additions & 0 deletions packages/@n8n/utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
25 changes: 25 additions & 0 deletions packages/@n8n/utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# @n8n/utils

A collection of utility functions that provide common functionality for both Front-End and Back-End packages.

## Table of Contents

- [Features](#features)
- [Contributing](#contributing)
- [License](#license)

## Features

- **Reusable Logic**: Build complex, stateful functionality using modular composable functions that you can easily reuse.
- **Consistent Patterns**: Enjoy a unified approach across n8n packages, making integration and maintenance a breeze.
- **Type-Safe & Reliable**: Benefit from TypeScript support, which improves the developer experience and code robustness.
- **Universal Functionality**: Designed to work seamlessly on both the front-end and back-end.
- **Easily Testable**: A modular design that simplifies testing, maintenance, and rapid development.

## Contributing

For more details, please read our [CONTRIBUTING.md](CONTRIBUTING.md).

## License

For more details, please read our [LICENSE.md](LICENSE.md).
4 changes: 4 additions & 0 deletions packages/@n8n/utils/biome.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "../../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../../biome.jsonc"]
}
40 changes: 40 additions & 0 deletions packages/@n8n/utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@n8n/utils",
"type": "module",
"version": "1.2.0",
"files": [
"dist"
],
"exports": {
"./*": {
"types": "./dist/*.d.ts",
"import": "./dist/*.js",
"require": "./dist/*.cjs"
}
},
"scripts": {
"dev": "vite",
"build": "pnpm run typecheck && tsup",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:dev": "vitest --silent=false",
"lint": "eslint src --ext .js,.ts,.vue --quiet",
"lintfix": "eslint src --ext .js,.ts,.vue --fix",
"format": "biome format --write . && prettier --write . --ignore-path ../../../.prettierignore",
"format:check": "biome ci . && prettier --check . --ignore-path ../../../.prettierignore"
},
"devDependencies": {
"@n8n/eslint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@testing-library/jest-dom": "catalog:frontend",
"@testing-library/user-event": "catalog:frontend",
"tsup": "catalog:frontend",
"typescript": "catalog:frontend",
"vite": "catalog:frontend",
"vite-plugin-dts": "catalog:frontend",
"vitest": "catalog:frontend"
},
"license": "See LICENSE.md file in the root of the repository"
}
1 change: 1 addition & 0 deletions packages/@n8n/utils/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
export function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error(message ?? 'Assertion failed');
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ type Payloads<ListenerMap> = {

type Listener<Payload> = (payload: Payload) => void;

// TODO: Fix all usages of `createEventBus` and convert `any` to `unknown`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface EventBus<ListenerMap extends Payloads<ListenerMap> = Record<string, any>> {
on<EventName extends keyof ListenerMap & string>(
Expand Down Expand Up @@ -42,7 +41,6 @@ export interface EventBus<ListenerMap extends Payloads<ListenerMap> = Record<str
* }>();
*/
export function createEventBus<
// TODO: Fix all usages of `createEventBus` and convert `any` to `unknown`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ListenerMap extends Payloads<ListenerMap> = Record<string, any>,
>(): EventBus<ListenerMap> {
Expand Down Expand Up @@ -77,7 +75,9 @@ export function createEventBus<
emit(eventName, event) {
const eventFns = handlers.get(eventName);
if (eventFns) {
eventFns.slice().forEach((handler) => handler(event));
eventFns.slice().forEach((handler) => {
handler(event);
});
}
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/*
Constants and utility functions used for searching for node types in node creator component
*/

// based on https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js
* Constants and utility functions used for searching for node types in node creator component
* based on https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js
*/

const SEQUENTIAL_BONUS = 60; // bonus for adjacent matches
const SEPARATOR_BONUS = 30; // bonus if match occurs after a separator
Expand Down Expand Up @@ -34,33 +33,6 @@ function fuzzyMatchSimple(pattern: string, target: string): boolean {
return pattern.length !== 0 && target.length !== 0 && patternIdx === pattern.length;
}

/**
* Does a fuzzy search to find pattern inside a string.
* @param {*} pattern string pattern to search for
* @param {*} target string string which is being searched
* @returns [boolean, number] a boolean which tells if pattern was
* found or not and a search score
*/
function fuzzyMatch(pattern: string, target: string): { matched: boolean; outScore: number } {
const recursionCount = 0;
const recursionLimit = 5;
const matches: number[] = [];
const maxMatches = 256;

return fuzzyMatchRecursive(
pattern,
target,
0 /* patternCurIndex */,
0 /* strCurrIndex */,
null /* srcMatces */,
matches,
maxMatches,
0 /* nextMatch */,
recursionCount,
recursionLimit,
);
}

function fuzzyMatchRecursive(
pattern: string,
target: string,
Expand Down Expand Up @@ -195,6 +167,33 @@ function fuzzyMatchRecursive(
return { matched: false, outScore };
}

/**
* Does a fuzzy search to find pattern inside a string.
* @param {*} pattern string pattern to search for
* @param {*} target string string which is being searched
* @returns [boolean, number] a boolean which tells if pattern was
* found or not and a search score
*/
function fuzzyMatch(pattern: string, target: string): { matched: boolean; outScore: number } {
const recursionCount = 0;
const recursionLimit = 5;
const matches: number[] = [];
const maxMatches = 256;

return fuzzyMatchRecursive(
pattern,
target,
0 /* patternCurIndex */,
0 /* strCurrIndex */,
null /* srcMatces */,
matches,
maxMatches,
0 /* nextMatch */,
recursionCount,
recursionLimit,
);
}

// prop = 'key'
// prop = 'key1.key2'
// prop = ['key1', 'key2']
Expand Down Expand Up @@ -225,6 +224,7 @@ export function sublimeSearch<T extends object>(
keys.forEach(({ key, weight }) => {
const value = getValue(item, key);
if (Array.isArray(value)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
values = values.concat(value.map((v) => ({ value: v, weight })));
} else if (typeof value === 'string') {
values.push({
Expand All @@ -237,24 +237,24 @@ export function sublimeSearch<T extends object>(
// for each item, check every key and get maximum score
const itemMatch = values.reduce(
(
accu: null | { matched: boolean; outScore: number },
result: null | { matched: boolean; outScore: number },
{ value, weight }: { value: string; weight: number },
) => {
if (!fuzzyMatchSimple(filter, value)) {
return accu;
return result;
}

const match = fuzzyMatch(filter, value);
match.outScore *= weight;

const { matched, outScore } = match;
if (!accu && matched) {
if (!result && matched) {
return match;
}
if (matched && accu && outScore > accu.outScore) {
if (matched && result && outScore > result.outScore) {
return match;
}
return accu;
return result;
},
null,
);
Expand All @@ -275,16 +275,3 @@ export function sublimeSearch<T extends object>(

return results;
}

export const sortByProperty = <T>(
property: keyof T,
arr: T[],
order: 'asc' | 'desc' = 'asc',
): T[] =>
arr.sort((a, b) => {
const result = String(a[property]).localeCompare(String(b[property]), undefined, {
numeric: true,
sensitivity: 'base',
});
return order === 'asc' ? result : -result;
});
1 change: 1 addition & 0 deletions packages/@n8n/utils/src/shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sortByProperty } from '@/utils/sortUtils';
import { sortByProperty } from './sortByProperty';

const arrayOfObjects = [
{ name: 'Álvaro', age: 30 },
Expand All @@ -7,8 +7,8 @@ const arrayOfObjects = [
{ name: 'Bob', age: 35 },
];

describe('sortUtils', () => {
it('"sortByProperty" should sort an array of objects by a property', () => {
describe('sortByProperty', () => {
it('should sort an array of objects by a property', () => {
const sortedArray = sortByProperty('name', arrayOfObjects);
expect(sortedArray).toEqual([
{ name: 'Álvaro', age: 30 },
Expand All @@ -18,7 +18,7 @@ describe('sortUtils', () => {
]);
});

it('"sortByProperty" should sort an array of objects by a property in descending order', () => {
it('should sort an array of objects by a property in descending order', () => {
const sortedArray = sortByProperty('name', arrayOfObjects, 'desc');
expect(sortedArray).toEqual([
{ name: 'Željko', age: 25 },
Expand All @@ -28,7 +28,7 @@ describe('sortUtils', () => {
]);
});

it('"sortByProperty" should sort an array of objects by a property if its number', () => {
it('should sort an array of objects by a property if its number', () => {
const sortedArray = sortByProperty('age', arrayOfObjects);
expect(sortedArray).toEqual([
{ name: 'Željko', age: 25 },
Expand All @@ -38,7 +38,7 @@ describe('sortUtils', () => {
]);
});

it('"sortByProperty" should sort an array of objects by a property in descending order if its number', () => {
it('should sort an array of objects by a property in descending order if its number', () => {
const sortedArray = sortByProperty('age', arrayOfObjects, 'desc');
expect(sortedArray).toEqual([
{ name: 'Bob', age: 35 },
Expand Down
12 changes: 12 additions & 0 deletions packages/@n8n/utils/src/sort/sortByProperty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const sortByProperty = <T>(
property: keyof T,
arr: T[],
order: 'asc' | 'desc' = 'asc',
): T[] =>
arr.sort((a, b) => {
const result = String(a[property]).localeCompare(String(b[property]), undefined, {
numeric: true,
sensitivity: 'base',
});
return order === 'asc' ? result : -result;
});
15 changes: 15 additions & 0 deletions packages/@n8n/utils/src/string/truncate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { truncate } from './truncate';

describe('truncate', () => {
it('should truncate text to 30 chars by default', () => {
expect(truncate('This is a very long text that should be truncated')).toBe(
'This is a very long text that ...',
);
});

it('should truncate text to given length', () => {
expect(truncate('This is a very long text that should be truncated', 25)).toBe(
'This is a very long text ...',
);
});
});
File renamed without changes.
11 changes: 11 additions & 0 deletions packages/@n8n/utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
"outDir": "dist",
"types": ["vite/client", "vitest/globals"],
"isolatedModules": true
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts", "tsup.config.ts"]
}
11 changes: 11 additions & 0 deletions packages/@n8n/utils/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__**/*'],
format: ['cjs', 'esm'],
clean: true,
dts: true,
cjsInterop: true,
splitting: true,
sourcemap: true,
});
4 changes: 4 additions & 0 deletions packages/@n8n/utils/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig, mergeConfig } from 'vite';
import { vitestConfig } from '@n8n/vitest-config/frontend';

export default mergeConfig(defineConfig({}), vitestConfig);
1 change: 1 addition & 0 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"@n8n/composables": "workspace:*",
"@n8n/utils": "workspace:*",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.3",
Expand Down
Loading

0 comments on commit 7fb88e6

Please sign in to comment.