diff --git a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.test.ts b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.test.ts new file mode 100644 index 00000000000..0754e079b90 --- /dev/null +++ b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.test.ts @@ -0,0 +1,379 @@ +import { describe, test, expect } from 'vitest'; +import { sortCompletionsByInput } from './datatype.completions'; +import type { AliasCompletion } from 'n8n-workflow'; + +describe('sortCompletionsByInput', () => { + describe('prefix match on label (order 3)', () => { + test('should assign order 3 when input matches label prefix', () => { + const options: AliasCompletion[] = [ + { label: 'push()', type: 'method' }, + { label: 'pop()', type: 'method' }, + ]; + + const result = sortCompletionsByInput(options, 'pu'); + + expect(result[0]).toEqual({ + option: options[0], + order: 3, + alias: undefined, + }); + expect(result[1]).toEqual({ + option: options[1], + order: 0, + alias: undefined, + }); + }); + + test('should assign order 3 for exact label match', () => { + const options: AliasCompletion[] = [{ label: 'push()', type: 'method' }]; + + const result = sortCompletionsByInput(options, 'push()'); + + expect(result[0]).toEqual({ + option: options[0], + order: 3, + alias: undefined, + }); + }); + + test('should be case-insensitive for prefix matching', () => { + const options: AliasCompletion[] = [{ label: 'toLowerCase()', type: 'method' }]; + + const result = sortCompletionsByInput(options, 'tolower'); + + expect(result[0].order).toBe(3); + }); + }); + + describe('exact alias match (order 2)', () => { + test('should assign order 2 when input exactly matches an alias with mode "exact"', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [{ label: 'append', mode: 'exact' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'append'); + + expect(result[0]).toEqual({ + option: options[0], + order: 2, + alias: { label: 'append', mode: 'exact' }, + }); + }); + + test('should prefer exact alias match over prefix label match', () => { + const options: AliasCompletion[] = [ + { + label: 'append()', + type: 'method', + alias: [{ label: 'push', mode: 'exact' }], + }, + { + label: 'push()', + type: 'method', + }, + ]; + + const result = sortCompletionsByInput(options, 'push'); + + // First should be exact alias match (order 2) even though second has prefix match (order 3) + // Wait, order 3 is higher than order 2, so prefix match wins + expect(result[0].option.label).toBe('push()'); + expect(result[0].order).toBe(3); + expect(result[1].option.label).toBe('append()'); + expect(result[1].order).toBe(2); + }); + + test('should not match if alias mode is not "exact"', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [{ label: 'append', mode: 'prefix' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'append'); + + // Should fall through to prefix alias match, not exact + expect(result[0].order).toBe(1); + }); + + test('should handle multiple aliases and match the exact one', () => { + const options: AliasCompletion[] = [ + { + label: 'toUpperCase()', + type: 'method', + alias: [ + { label: 'upper', mode: 'prefix' }, + { label: 'UPPER', mode: 'exact' }, + ], + }, + ]; + + const result = sortCompletionsByInput(options, 'UPPER'); + + expect(result[0].order).toBe(2); + expect(result[0].alias).toEqual({ label: 'UPPER', mode: 'exact' }); + }); + }); + + describe('prefix alias match (order 1)', () => { + test('should assign order 1 when input matches alias prefix and mode is not "exact"', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [{ label: 'append', mode: 'prefix' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'app'); + + expect(result[0]).toEqual({ + option: options[0], + order: 1, + alias: { label: 'append', mode: 'prefix' }, + }); + }); + + test('should match when alias has no mode specified (defaults to prefix)', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [{ label: 'append' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'app'); + + expect(result[0].order).toBe(1); + expect(result[0].alias).toEqual({ label: 'append' }); + }); + + test('should match first prefix alias when multiple exist', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [{ label: 'append' }, { label: 'add' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'app'); + + expect(result[0].order).toBe(1); + expect(result[0].alias).toEqual({ label: 'append' }); + }); + }); + + describe('no match (order 0)', () => { + test('should assign order 0 when no matches found', () => { + const options: AliasCompletion[] = [ + { label: 'push()', type: 'method' }, + { label: 'pop()', type: 'method' }, + ]; + + const result = sortCompletionsByInput(options, 'xyz'); + + expect(result[0].order).toBe(0); + expect(result[1].order).toBe(0); + }); + + test('should handle empty alias array', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [], + }, + ]; + + const result = sortCompletionsByInput(options, 'app'); + + expect(result[0].order).toBe(0); + }); + }); + + describe('sorting behavior', () => { + test('should sort by order descending (highest first)', () => { + const options: AliasCompletion[] = [ + { + label: 'slice()', + type: 'method', + }, + { + label: 'append()', + type: 'method', + alias: [{ label: 'push', mode: 'exact' }], + }, + { + label: 'push()', + type: 'method', + alias: [{ label: 'add' }], + }, + { + label: 'map()', + type: 'method', + }, + ]; + + const result = sortCompletionsByInput(options, 'push'); + + // push() has prefix match on label (order 3) - should be first + // append() has exact alias match (order 2) - should be second + // slice() and map() have no match (order 0) - should be last + expect(result[0].option.label).toBe('push()'); // order 3 + expect(result[0].order).toBe(3); + expect(result[1].option.label).toBe('append()'); // order 2 + expect(result[1].order).toBe(2); + expect(result[2].option.label).toBe('slice()'); // order 0 + expect(result[2].order).toBe(0); + expect(result[3].option.label).toBe('map()'); // order 0 + expect(result[3].order).toBe(0); + }); + + test('should maintain order for items with same priority', () => { + const options: AliasCompletion[] = [ + { label: 'pop()', type: 'method' }, + { label: 'pow()', type: 'method' }, + ]; + + const result = sortCompletionsByInput(options, 'po'); + + expect(result[0].order).toBe(3); + expect(result[1].order).toBe(3); + // Original order should be preserved for same priority + expect(result[0].option.label).toBe('pop()'); + expect(result[1].option.label).toBe('pow()'); + }); + }); + + describe('complex scenarios', () => { + test('should handle all order types together', () => { + const options: AliasCompletion[] = [ + { + label: 'splice()', + type: 'method', + alias: [{ label: 'remove', mode: 'exact' }], + }, + { + label: 'split()', + type: 'method', + alias: [{ label: 'explode' }], + }, + { + label: 'slice()', + type: 'method', + }, + { + label: 'concat()', + type: 'method', + }, + ]; + + const result = sortCompletionsByInput(options, 'spl'); + + // split() and splice() have prefix match on label (order 3) + // Others have no match (order 0) + expect(result[0].order).toBe(3); + expect(result[1].order).toBe(3); + expect(result[2].order).toBe(0); + expect(result[3].order).toBe(0); + }); + + test('should prioritize label over alias', () => { + const options: AliasCompletion[] = [ + { + label: 'append()', + type: 'method', + alias: [{ label: 'push' }], + }, + { + label: 'push()', + type: 'method', + alias: [{ label: 'append' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'push'); + + // push() has prefix match on label (order 3) - should be first + // append() has prefix alias match (order 1) - should be second + expect(result[0].option.label).toBe('push()'); + expect(result[0].order).toBe(3); + expect(result[1].option.label).toBe('append()'); + expect(result[1].order).toBe(1); + }); + + test('should handle empty input string', () => { + const options: AliasCompletion[] = [ + { label: 'push()', type: 'method' }, + { label: 'pop()', type: 'method' }, + ]; + + const result = sortCompletionsByInput(options, ''); + + // Empty string should match all as prefix + expect(result[0].order).toBe(3); + expect(result[1].order).toBe(3); + }); + + test('should handle empty options array', () => { + const options: AliasCompletion[] = []; + + const result = sortCompletionsByInput(options, 'test'); + + expect(result).toEqual([]); + }); + + test('should handle options without type property', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + alias: [{ label: 'append' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'app'); + + expect(result[0].order).toBe(1); + }); + + test('should work with real-world array method aliases', () => { + const options: AliasCompletion[] = [ + { + label: 'push()', + type: 'method', + alias: [{ label: 'append' }, { label: 'add' }], + }, + { + label: 'pop()', + type: 'method', + alias: [{ label: 'remove' }], + }, + { + label: 'concat()', + type: 'method', + alias: [{ label: 'extend' }], + }, + { + label: 'includes()', + type: 'method', + alias: [{ label: 'contains' }], + }, + ]; + + const result = sortCompletionsByInput(options, 'app'); + + // push() should match via its 'append' alias (order 1) + expect(result[0].option.label).toBe('push()'); + expect(result[0].order).toBe(1); + expect(result[0].alias?.label).toBe('append'); + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.ts b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.ts index a6ef633368a..67452abc55b 100644 --- a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.ts +++ b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/datatype.completions.ts @@ -2,7 +2,6 @@ import { VALID_EMAIL_REGEX } from '@/app/constants'; import { i18n } from '@n8n/i18n'; import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store'; import { useExternalSecretsStore } from '@/features/integrations/externalSecrets.ee/externalSecrets.ee.store'; - import type { Completion, CompletionContext, @@ -11,8 +10,14 @@ import type { } from '@codemirror/autocomplete'; import uniqBy from 'lodash/uniqBy'; import { DateTime } from 'luxon'; -import type { DocMetadata, IDataObject, NativeDoc } from 'n8n-workflow'; -import { Expression, ExpressionExtensions, NativeMethods, validateFieldType } from 'n8n-workflow'; +import type { Alias, DocMetadata, IDataObject, NativeDoc } from 'n8n-workflow'; +import { + Expression, + ExpressionExtensions, + NativeMethods, + validateFieldType, + type AliasCompletion, +} from 'n8n-workflow'; import { ARRAY_NUMBER_ONLY_METHODS, ARRAY_RECOMMENDED_OPTIONS, @@ -74,7 +79,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul const syntaxTree = javascriptLanguage.parser.parse(word.text); const [base, tail] = splitBaseTail(syntaxTree, word.text); - let options: Completion[] = []; + let options: AliasCompletion[] = []; const isCredential = isCredentialsModalOpen(); @@ -116,7 +121,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul } if (tail !== '') { - options = options.filter((o) => prefixMatch(o.label, tail)); + options = filterOptions(options, tail); } let from = word.to - tail.length; @@ -142,10 +147,54 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul }; } +/** + * Returns a sorted array of completions by the input string. + * Each completion is returned with its order. + * 0 if no match + * 1 if prefix alias match + * 2 if exact alias match + * 3 if prefix label match + */ +export function sortCompletionsByInput(options: AliasCompletion[], input: string) { + return options + .map((o) => { + let order = 0; + let alias: Alias | undefined = undefined; + if (prefixMatch(o.label, input)) { + order = 3; + } else { + const exactAliasMatch = o.alias?.find((a) => a.mode === 'exact' && a.label === input); + const prefixAliasMatch = o.alias?.find( + (a) => a.mode !== 'exact' && prefixMatch(a.label, input), + ); + if (exactAliasMatch) { + alias = exactAliasMatch; + order = 2; + } else if (prefixAliasMatch) { + alias = prefixAliasMatch; + order = 1; + } + } + return { + option: o, + order, + alias, + }; + }) + .sort((a, b) => b.order - a.order); +} + +function filterOptions(options: AliasCompletion[], tail: string): AliasCompletion[] { + const matches = sortCompletionsByInput(options, tail) + .filter((m) => m.order > 0) + .map((m) => m.option); + return matches; +} + function explicitDataTypeOptions( expression: string, targetNodeParameterContext?: TargetNodeParameterContext, -): Completion[] { +): AliasCompletion[] { return attempt( () => { const resolved = resolveAutocompleteExpression( @@ -163,7 +212,7 @@ function explicitDataTypeOptions( ); } -function datatypeOptions(input: AutocompleteInput): Completion[] { +function datatypeOptions(input: AutocompleteInput): AliasCompletion[] { const { resolved } = input; if (resolved === null) return []; @@ -211,7 +260,7 @@ export const natives = ({ }: { typeName: ExtensionTypeName; transformLabel?: (label: string) => string; -}): Completion[] => { +}): AliasCompletion[] => { const nativeDocs = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName); if (!nativeDocs) return []; @@ -324,9 +373,9 @@ const createCompletionOption = ({ isFunction?: boolean; transformLabel?: (label: string) => string; type?: 'strikethrough'; -}): Completion => { +}): AliasCompletion => { const label = isFunction ? name + '()' : name; - const option: Completion = { + const option: AliasCompletion = { label, section: doc?.section, apply: applyCompletion({ @@ -334,6 +383,7 @@ const createCompletionOption = ({ defaultArgs: getDefaultArgs(doc), transformLabel, }), + alias: doc?.aliases?.map((alias) => ({ label: alias, mode: doc.aliasMode })), type, }; option.info = createInfoBoxRenderer(doc, isFunction); diff --git a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/types.ts b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/types.ts index 031eb12d6e8..0ab454f9e29 100644 --- a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/types.ts +++ b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/completions/types.ts @@ -4,7 +4,7 @@ export type Resolved = unknown; export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object' | 'boolean'; -export type FnToDoc = { [fnName: string]: { doc?: DocMetadata } }; +export type FnToDoc = { [fnName: string]: { doc?: DocMetadata; aliases?: string[] } }; export type FunctionOptionType = 'native-function' | 'extension-function'; export type KeywordOptionType = 'keyword'; diff --git a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/typescript/client/completions.ts b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/typescript/client/completions.ts index ba591996cf7..071fdc36bbf 100644 --- a/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/typescript/client/completions.ts +++ b/packages/frontend/editor-ui/src/features/shared/editors/plugins/codemirror/typescript/client/completions.ts @@ -6,14 +6,12 @@ import { type Completion, type CompletionSource, } from '@codemirror/autocomplete'; -import { - autocompletableNodeNames, - longestCommonPrefix, - prefixMatch, -} from '../../completions/utils'; +import { autocompletableNodeNames, longestCommonPrefix } from '../../completions/utils'; import { typescriptWorkerFacet } from './facet'; import { blockCommentSnippet, snippets } from './snippets'; import { TARGET_NODE_PARAMETER_FACET } from '../../completions/constants'; +import type { AliasCompletion, Alias } from 'n8n-workflow'; +import { sortCompletionsByInput } from '../../completions/datatype.completions'; const START_CHARACTERS = ['"', "'", '(', '.', '@']; const START_CHARACTERS_REGEX = /[\.\(\'\"\@]/; @@ -48,7 +46,49 @@ export const typescriptCompletionSource: CompletionSource = async (context) => { const { result, isGlobal } = completionResult; - let options = [...result.options]; + let options: AliasCompletion[] = [...result.options]; + + const addAlias = (label: string, alias: Alias[]) => { + const completionIndex = options.findIndex((opt) => opt.label === label); + if (completionIndex !== -1) { + options[completionIndex].alias = alias; + } + }; + + // array aliases + addAlias('push()', [ + { + label: 'append()', + info: 'Use push() to add one or more elements to the end of an array', + }, + ]); + addAlias('pop()', [ + { label: 'remove()', info: 'Use pop() to remove the last element from an array' }, + ]); + addAlias('splice()', [ + { + label: 'insertAt()', + info: 'Use splice(index, 0, value) to insert elements at a specified index in an array', + }, + { + label: 'removeAt()', + info: 'Use splice(indexOf(itemToRemove), 1) to remove elements from an array', + }, + ]); + addAlias('concat()', [{ label: 'extend()', info: 'Use concat() to merge two or more arrays' }]); + addAlias('includes()', [ + { label: 'contains()', info: 'Use includes() to check if an array contains a value' }, + { label: 'has()', info: 'Use includes() to check if an array contains a value' }, + ]); + + // string aliases + addAlias('toLowerCase()', [{ label: 'lower()' }]); + addAlias('toUpperCase()', [{ label: 'upper()' }]); + + addAlias('length', [ + { label: 'size', mode: 'exact' }, + { label: 'count', mode: 'exact' }, + ]); if (isGlobal) { options = options @@ -67,6 +107,22 @@ export const typescriptCompletionSource: CompletionSource = async (context) => { .concat(snippets); } + const filteredOptions: AliasCompletion[] = []; + for (const { option, order, alias } of sortCompletionsByInput(options, word.text)) { + const isEmpty = word.text === ''; + const isStartCharacter = START_CHARACTERS.includes(word.text); + const isMatch = order > 0; + + // replace the default description with the alias description + if (alias?.info) { + option.info = alias.info; + } + + if (isEmpty || isStartCharacter || isMatch) { + filteredOptions.push(option); + } + } + return { from: word ? (START_CHARACTERS.includes(word.text) ? word.to : word.from) : context.pos, filter: false, @@ -74,28 +130,18 @@ export const typescriptCompletionSource: CompletionSource = async (context) => { const lcp = longestCommonPrefix(completion.label, word.text); return [0, lcp.length]; }, - options: options - .filter( - (option) => - word.text === '' || - START_CHARACTERS.includes(word.text) || - prefixMatch( - option.label.replace(START_CHARACTERS_REGEX, ''), - word.text.replace(START_CHARACTERS_REGEX, ''), - ), - ) - .map((completion) => { - if (completion.label.endsWith('()')) { - completion.apply = (view, _, from, to) => { - const cursorPosition = from + completion.label.length - 1; - view.dispatch({ - ...insertCompletionText(view.state, completion.label, from, to), - annotations: pickedCompletion.of(completion), - selection: { anchor: cursorPosition, head: cursorPosition }, - }); - }; - } - return completion; - }), + options: filteredOptions.map((completion) => { + if (completion.label.endsWith('()')) { + completion.apply = (view, _, from, to) => { + const cursorPosition = from + completion.label.length - 1; + view.dispatch({ + ...insertCompletionText(view.state, completion.label, from, to), + annotations: pickedCompletion.of(completion), + selection: { anchor: cursorPosition, head: cursorPosition }, + }); + }; + } + return completion; + }), }; }; diff --git a/packages/workflow/src/extensions/array-extensions.ts b/packages/workflow/src/extensions/array-extensions.ts index 079743d977a..701d3ad5a58 100644 --- a/packages/workflow/src/extensions/array-extensions.ts +++ b/packages/workflow/src/extensions/array-extensions.ts @@ -334,6 +334,7 @@ export function toDateTime() { average.doc = { name: 'average', + aliases: ['mean'], description: 'Returns the average of the numbers in the array. Throws an error if there are any non-numbers.', examples: [{ example: '[12, 1, 5].average()', evaluated: '6' }], @@ -343,6 +344,7 @@ average.doc = { compact.doc = { name: 'compact', + aliases: ['removeEmpty'], description: 'Removes any empty values from the array. null, "" and undefined count as empty.', examples: [{ example: '[2, null, 1, ""].compact()', evaluated: '[2, 1]' }], @@ -374,6 +376,7 @@ isNotEmpty.doc = { first.doc = { name: 'first', + aliases: ['head'], description: 'Returns the first element of the array', examples: [{ example: "['quick', 'brown', 'fox'].first()", evaluated: "'quick'" }], returnType: 'any', @@ -382,6 +385,7 @@ first.doc = { last.doc = { name: 'last', + aliases: ['tail'], description: 'Returns the last element of the array', examples: [{ example: "['quick', 'brown', 'fox'].last()", evaluated: "'fox'" }], returnType: 'any', @@ -640,6 +644,7 @@ toJsonString.doc = { append.doc = { name: 'append', + aliases: ['push'], description: 'Adds new elements to the end of the array. Similar to push(), but returns the modified array. Consider using spread syntax instead (see examples).', examples: [ diff --git a/packages/workflow/src/extensions/extensions.ts b/packages/workflow/src/extensions/extensions.ts index 2a5ef9d33af..c1b55f063ae 100644 --- a/packages/workflow/src/extensions/extensions.ts +++ b/packages/workflow/src/extensions/extensions.ts @@ -1,3 +1,10 @@ +import type { Completion } from '@codemirror/autocomplete'; + +export type Alias = { label: string; info?: string; mode?: 'prefix' | 'exact' }; +export interface AliasCompletion extends Completion { + alias?: Alias[]; +} + export interface ExtensionMap { typeName: string; functions: Record; @@ -35,6 +42,7 @@ export type DocMetadata = { section?: string; hidden?: boolean; aliases?: string[]; + aliasMode?: 'prefix' | 'exact'; args?: DocMetadataArgument[]; examples?: DocMetadataExample[]; docURL?: string; diff --git a/packages/workflow/src/extensions/index.ts b/packages/workflow/src/extensions/index.ts index 3824acacf85..1445fb2af47 100644 --- a/packages/workflow/src/extensions/index.ts +++ b/packages/workflow/src/extensions/index.ts @@ -14,3 +14,8 @@ export type { DocMetadataArgument, DocMetadataExample, } from './extensions'; + +export type { + Alias, + AliasCompletion, +} from './extensions'; diff --git a/packages/workflow/src/extensions/object-extensions.ts b/packages/workflow/src/extensions/object-extensions.ts index 3dacff99f1b..954aa9232fa 100644 --- a/packages/workflow/src/extensions/object-extensions.ts +++ b/packages/workflow/src/extensions/object-extensions.ts @@ -179,6 +179,7 @@ hasField.doc = { removeField.doc = { name: 'removeField', + aliases: ['delete'], description: "Removes a field from the Object. The same as JavaScript's delete.", examples: [ { diff --git a/packages/workflow/src/extensions/string-extensions.ts b/packages/workflow/src/extensions/string-extensions.ts index 1bb15bd380c..974edc9862b 100644 --- a/packages/workflow/src/extensions/string-extensions.ts +++ b/packages/workflow/src/extensions/string-extensions.ts @@ -821,6 +821,7 @@ quote.doc = { parseJson.doc = { name: 'parseJson', + aliases: ['fromJson'], description: "Returns the JavaScript value or object represented by the string, or undefined if the string isn't valid JSON. Single-quoted JSON is not supported.", section: 'cast', @@ -836,6 +837,7 @@ parseJson.doc = { base64Encode.doc = { name: 'base64Encode', + aliases: ['toBase64'], description: 'Converts plain text to a base64-encoded string', examples: [{ example: '"hello".base64Encode()', evaluated: '"aGVsbG8="' }], section: 'edit', @@ -846,6 +848,7 @@ base64Encode.doc = { base64Decode.doc = { name: 'base64Decode', + aliases: ['fromBase64'], description: 'Converts a base64-encoded string to plain text', examples: [{ example: '"aGVsbG8=".base64Decode()', evaluated: '"hello"' }], section: 'edit', diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index f5d6d204514..95a41dc954a 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -75,7 +75,7 @@ export { type ExtractableSubgraphData, type IConnectionAdjacencyList as AdjacencyList, } from './graph/graph-utils'; -export { ExpressionExtensions } from './extensions'; +export { ExpressionExtensions, type Alias, type AliasCompletion } from './extensions'; export * as ExpressionParser from './extensions/expression-parser'; export { NativeMethods } from './native-methods'; export * from './node-parameters/filter-parameter'; diff --git a/packages/workflow/src/native-methods/array.methods.ts b/packages/workflow/src/native-methods/array.methods.ts index b21cd5d9b4c..d6f13495754 100644 --- a/packages/workflow/src/native-methods/array.methods.ts +++ b/packages/workflow/src/native-methods/array.methods.ts @@ -6,6 +6,8 @@ export const arrayMethods: NativeDoc = { length: { doc: { name: 'length', + aliases: ['size', 'count'], + aliasMode: 'exact', description: 'The number of elements in the array', examples: [{ example: "['Bob', 'Bill', 'Nat'].length", evaluated: '3' }], docURL: @@ -18,6 +20,7 @@ export const arrayMethods: NativeDoc = { concat: { doc: { name: 'concat', + aliases: ['extend'], description: 'Joins one or more arrays onto the end of the base array', examples: [ { @@ -239,6 +242,7 @@ export const arrayMethods: NativeDoc = { includes: { doc: { name: 'includes', + aliases: ['contains', 'has'], description: 'Returns true if the array contains the specified element', examples: [ { example: "['Bob', 'Bill', 'Nat'].includes('Nat')", evaluated: 'true' }, @@ -418,6 +422,7 @@ export const arrayMethods: NativeDoc = { description: 'Returns a portion of the array, from the start index up to (but not including) the end index. Indexes start at 0.', examples: [ + { example: '[1, 2, 3, 4, 5].slice(0, -1)', evaluated: '[1, 2, 3, 4]' }, { example: '[1, 2, 3, 4, 5].slice(2, 4)', evaluated: '[3, 4]' }, { example: '[1, 2, 3, 4, 5].slice(2)', evaluated: '[3, 4, 5]' }, { example: '[1, 2, 3, 4, 5].slice(-2)', evaluated: '[4, 5]' }, @@ -536,6 +541,7 @@ export const arrayMethods: NativeDoc = { toSpliced: { doc: { name: 'toSpliced', + aliases: ['insertAt', 'removeAt'], description: 'Adds and/or removes array elements at a given position. \n\nSee also slice() and append().', examples: [ diff --git a/packages/workflow/src/native-methods/string.methods.ts b/packages/workflow/src/native-methods/string.methods.ts index c49ffdd7569..cebe6217bc6 100644 --- a/packages/workflow/src/native-methods/string.methods.ts +++ b/packages/workflow/src/native-methods/string.methods.ts @@ -6,6 +6,8 @@ export const stringMethods: NativeDoc = { length: { doc: { name: 'length', + aliases: ['size', 'count'], + aliasMode: 'exact', description: 'The number of characters in the string', examples: [{ example: '"hello".length', evaluated: '5' }], section: 'query', @@ -190,6 +192,7 @@ export const stringMethods: NativeDoc = { includes: { doc: { name: 'includes', + aliases: ['contains'], description: 'Returns true if the string contains the searchString. Case-sensitive.', section: 'query', @@ -483,6 +486,7 @@ export const stringMethods: NativeDoc = { toLowerCase: { doc: { name: 'toLowerCase', + aliases: ['lower'], description: 'Converts all letters in the string to lower case', section: 'case', docURL: @@ -494,6 +498,7 @@ export const stringMethods: NativeDoc = { toUpperCase: { doc: { name: 'toUpperCase', + aliases: ['upper'], description: 'Converts all letters in the string to upper case (capitals)', examples: [{ example: '"I\'m not angry".toUpperCase()', evaluated: '"I\'M NOT ANGRY"' }], section: 'case', @@ -517,6 +522,7 @@ export const stringMethods: NativeDoc = { trimEnd: { doc: { name: 'trimEnd', + aliases: ['trimRight'], description: 'Removes whitespace from the end of a string and returns a new string. Whitespace includes new lines, tabs, spaces, etc.', examples: [{ example: "' lonely '.trimEnd()", evaluated: "' lonely'" }], @@ -529,6 +535,7 @@ export const stringMethods: NativeDoc = { trimStart: { doc: { name: 'trimStart', + aliases: ['trimLeft'], description: 'Removes whitespace from the beginning of a string and returns a new string. Whitespace includes new lines, tabs, spaces, etc.', examples: [{ example: "' lonely '.trimStart()", evaluated: "'lonely '" }],