mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add missing extension methods for expressions (#8845)
This commit is contained in:
parent
7176cd1407
commit
5e84c2ab89
|
@ -1235,6 +1235,7 @@ export interface NDVState {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
focusedMappableInput: string;
|
focusedMappableInput: string;
|
||||||
|
focusedInputPath: string;
|
||||||
mappingTelemetry: { [key: string]: string | number | boolean };
|
mappingTelemetry: { [key: string]: string | number | boolean };
|
||||||
hoveringItem: null | TargetItem;
|
hoveringItem: null | TargetItem;
|
||||||
draggable: {
|
draggable: {
|
||||||
|
|
|
@ -230,6 +230,7 @@ export default defineComponent({
|
||||||
if (!this.parameter.noDataExpression) {
|
if (!this.parameter.noDataExpression) {
|
||||||
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
||||||
}
|
}
|
||||||
|
this.ndvStore.setFocusedInputPath(this.path ?? '');
|
||||||
},
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.focused = false;
|
this.focused = false;
|
||||||
|
@ -239,6 +240,7 @@ export default defineComponent({
|
||||||
) {
|
) {
|
||||||
this.ndvStore.setMappableNDVInputFocus('');
|
this.ndvStore.setMappableNDVInputFocus('');
|
||||||
}
|
}
|
||||||
|
this.ndvStore.setFocusedInputPath('');
|
||||||
this.$emit('blur');
|
this.$emit('blur');
|
||||||
},
|
},
|
||||||
onMenuExpanded(expanded: boolean) {
|
onMenuExpanded(expanded: boolean) {
|
||||||
|
|
|
@ -176,13 +176,17 @@ export function resolveParameter(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (activeNode?.type === HTTP_REQUEST_NODE_TYPE) {
|
if (activeNode?.type === HTTP_REQUEST_NODE_TYPE) {
|
||||||
// Add $response for HTTP Request-Nodes as it is used
|
const EMPTY_RESPONSE = { statusCode: 200, headers: {}, body: {} };
|
||||||
|
const EMPTY_REQUEST = { headers: {}, body: {}, qs: {} };
|
||||||
|
// Add $request,$response,$pageCount for HTTP Request-Nodes as it is used
|
||||||
// in pagination expressions
|
// in pagination expressions
|
||||||
|
additionalKeys.$pageCount = 0;
|
||||||
additionalKeys.$response = get(
|
additionalKeys.$response = get(
|
||||||
executionData,
|
executionData,
|
||||||
['data', 'executionData', 'contextData', `node:${activeNode.name}`, 'response'],
|
['data', 'executionData', 'contextData', `node:${activeNode.name}`, 'response'],
|
||||||
{},
|
EMPTY_RESPONSE,
|
||||||
);
|
);
|
||||||
|
additionalKeys.$request = EMPTY_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
let runIndexCurrent = opts?.targetItem?.runIndex ?? 0;
|
let runIndexCurrent = opts?.targetItem?.runIndex ?? 0;
|
||||||
|
|
|
@ -545,16 +545,16 @@ describe('Resolution-based completions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('recommended completions', () => {
|
describe('recommended completions', () => {
|
||||||
test('should recommended toDate() for {{ "1-Feb-2024".| }}', () => {
|
test('should recommend toDateTime() for {{ "1-Feb-2024".| }}', () => {
|
||||||
// @ts-expect-error Spied function is mistyped
|
// @ts-expect-error Spied function is mistyped
|
||||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('1-Feb-2024');
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('1-Feb-2024');
|
||||||
|
|
||||||
expect(completions('{{ "1-Feb-2024".| }}')?.[0]).toEqual(
|
expect(completions('{{ "1-Feb-2024".| }}')?.[0]).toEqual(
|
||||||
expect.objectContaining({ label: 'toDate()', section: RECOMMENDED_SECTION }),
|
expect.objectContaining({ label: 'toDateTime()', section: RECOMMENDED_SECTION }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should recommended toInt(),toFloat() for: {{ "5.3".| }}', () => {
|
test('should recommend toInt(),toFloat() for: {{ "5.3".| }}', () => {
|
||||||
// @ts-expect-error Spied function is mistyped
|
// @ts-expect-error Spied function is mistyped
|
||||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('5.3');
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('5.3');
|
||||||
const options = completions('{{ "5.3".| }}');
|
const options = completions('{{ "5.3".| }}');
|
||||||
|
@ -566,7 +566,7 @@ describe('Resolution-based completions', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should recommended extractEmail() for: {{ "string with test@n8n.io in it".| }}', () => {
|
test('should recommend extractEmail() for: {{ "string with test@n8n.io in it".| }}', () => {
|
||||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
// @ts-expect-error Spied function is mistyped
|
// @ts-expect-error Spied function is mistyped
|
||||||
'string with test@n8n.io in it',
|
'string with test@n8n.io in it',
|
||||||
|
@ -577,7 +577,7 @@ describe('Resolution-based completions', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should recommended extractDomain() for: {{ "test@n8n.io".| }}', () => {
|
test('should recommend extractDomain(), isEmail() for: {{ "test@n8n.io".| }}', () => {
|
||||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
// @ts-expect-error Spied function is mistyped
|
// @ts-expect-error Spied function is mistyped
|
||||||
'test@n8n.io',
|
'test@n8n.io',
|
||||||
|
@ -586,9 +586,26 @@ describe('Resolution-based completions', () => {
|
||||||
expect(options?.[0]).toEqual(
|
expect(options?.[0]).toEqual(
|
||||||
expect.objectContaining({ label: 'extractDomain()', section: RECOMMENDED_SECTION }),
|
expect.objectContaining({ label: 'extractDomain()', section: RECOMMENDED_SECTION }),
|
||||||
);
|
);
|
||||||
|
expect(options?.[1]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'isEmail()', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should recommended round(),floor(),ceil() for: {{ (5.46).| }}', () => {
|
test('should recommend extractDomain(), extractUrlPath() for: {{ "https://n8n.io/pricing".| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
'https://n8n.io/pricing',
|
||||||
|
);
|
||||||
|
const options = completions('{{ "https://n8n.io/pricing".| }}');
|
||||||
|
expect(options?.[0]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'extractDomain()', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
|
expect(options?.[1]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'extractUrlPath()', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should recommend round(),floor(),ceil() for: {{ (5.46).| }}', () => {
|
||||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
// @ts-expect-error Spied function is mistyped
|
// @ts-expect-error Spied function is mistyped
|
||||||
5.46,
|
5.46,
|
||||||
|
@ -604,6 +621,50 @@ describe('Resolution-based completions', () => {
|
||||||
expect.objectContaining({ label: 'ceil()', section: RECOMMENDED_SECTION }),
|
expect.objectContaining({ label: 'ceil()', section: RECOMMENDED_SECTION }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should recommend toDateTime("s") for: {{ (1900062210).| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
1900062210,
|
||||||
|
);
|
||||||
|
const options = completions('{{ (1900062210).| }}');
|
||||||
|
expect(options?.[0]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'toDateTime("s")', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should recommend toDateTime("ms") for: {{ (1900062210000).| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
1900062210000,
|
||||||
|
);
|
||||||
|
const options = completions('{{ (1900062210000).| }}');
|
||||||
|
expect(options?.[0]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'toDateTime("ms")', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should recommend toBoolean() for: {{ (0).| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const options = completions('{{ (0).| }}');
|
||||||
|
expect(options?.[0]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'toBoolean()', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should recommend toBoolean() for: {{ "true".| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
'true',
|
||||||
|
);
|
||||||
|
const options = completions('{{ "true".| }}');
|
||||||
|
expect(options?.[0]).toEqual(
|
||||||
|
expect.objectContaining({ label: 'toBoolean()', section: RECOMMENDED_SECTION }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('explicit completions (opened by Ctrl+Space or programatically)', () => {
|
describe('explicit completions (opened by Ctrl+Space or programatically)', () => {
|
||||||
|
|
|
@ -145,7 +145,7 @@ export const STRING_RECOMMENDED_OPTIONS = [
|
||||||
|
|
||||||
export const DATE_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'extract()'];
|
export const DATE_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'extract()'];
|
||||||
export const LUXON_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'diff()', 'extract()'];
|
export const LUXON_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'diff()', 'extract()'];
|
||||||
export const OBJECT_RECOMMENDED_OPTIONS = ['keys()', 'values()', 'isEmpty()'];
|
export const OBJECT_RECOMMENDED_OPTIONS = ['keys()', 'values()', 'isEmpty()', 'hasField()'];
|
||||||
export const ARRAY_RECOMMENDED_OPTIONS = ['length', 'last()', 'includes()', 'map()', 'filter()'];
|
export const ARRAY_RECOMMENDED_OPTIONS = ['length', 'last()', 'includes()', 'map()', 'filter()'];
|
||||||
export const ARRAY_NUMBER_ONLY_METHODS = ['max()', 'min()', 'sum()', 'average()'];
|
export const ARRAY_NUMBER_ONLY_METHODS = ['max()', 'min()', 'sum()', 'average()'];
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {
|
||||||
applyCompletion,
|
applyCompletion,
|
||||||
sortCompletionsAlpha,
|
sortCompletionsAlpha,
|
||||||
hasRequiredArgs,
|
hasRequiredArgs,
|
||||||
|
getDefaultArgs,
|
||||||
|
insertDefaultArgs,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import type {
|
import type {
|
||||||
Completion,
|
Completion,
|
||||||
|
@ -155,6 +157,10 @@ function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||||
return stringOptions(input as AutocompleteInput<string>);
|
return stringOptions(input as AutocompleteInput<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof resolved === 'boolean') {
|
||||||
|
return booleanOptions();
|
||||||
|
}
|
||||||
|
|
||||||
if (resolved instanceof DateTime) {
|
if (resolved instanceof DateTime) {
|
||||||
return luxonOptions(input as AutocompleteInput<DateTime>);
|
return luxonOptions(input as AutocompleteInput<DateTime>);
|
||||||
}
|
}
|
||||||
|
@ -239,7 +245,7 @@ export const toOptions = (
|
||||||
) => {
|
) => {
|
||||||
return Object.entries(fnToDoc)
|
return Object.entries(fnToDoc)
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.filter(([, docInfo]) => !docInfo.doc?.hidden || includeHidden)
|
.filter(([, docInfo]) => (docInfo.doc && !docInfo.doc?.hidden) || includeHidden)
|
||||||
.map(([fnName, docInfo]) => {
|
.map(([fnName, docInfo]) => {
|
||||||
return createCompletionOption(typeName, fnName, optionType, docInfo, transformLabel);
|
return createCompletionOption(typeName, fnName, optionType, docInfo, transformLabel);
|
||||||
});
|
});
|
||||||
|
@ -258,7 +264,11 @@ const createCompletionOption = (
|
||||||
label,
|
label,
|
||||||
type: optionType,
|
type: optionType,
|
||||||
section: docInfo.doc?.section,
|
section: docInfo.doc?.section,
|
||||||
apply: applyCompletion(hasRequiredArgs(docInfo?.doc), transformLabel),
|
apply: applyCompletion({
|
||||||
|
hasArgs: hasRequiredArgs(docInfo?.doc),
|
||||||
|
defaultArgs: getDefaultArgs(docInfo?.doc),
|
||||||
|
transformLabel,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
option.info = () => {
|
option.info = () => {
|
||||||
|
@ -395,8 +405,8 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||||
label: isFunction ? key + '()' : key,
|
label: isFunction ? key + '()' : key,
|
||||||
type: isFunction ? 'function' : 'keyword',
|
type: isFunction ? 'function' : 'keyword',
|
||||||
section: getObjectPropertySection({ name, key, isFunction }),
|
section: getObjectPropertySection({ name, key, isFunction }),
|
||||||
|
apply: applyCompletion({ hasArgs, transformLabel }),
|
||||||
detail: getDetail(name, resolvedProp),
|
detail: getDetail(name, resolvedProp),
|
||||||
apply: applyCompletion(hasArgs, transformLabel),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoKey = [name, key].join('.');
|
const infoKey = [name, key].join('.');
|
||||||
|
@ -466,7 +476,7 @@ const applySections = ({
|
||||||
recommendedSection = RECOMMENDED_SECTION,
|
recommendedSection = RECOMMENDED_SECTION,
|
||||||
}: {
|
}: {
|
||||||
options: Completion[];
|
options: Completion[];
|
||||||
recommended?: string[];
|
recommended?: Array<string | { label: string; args: unknown[] }>;
|
||||||
recommendedSection?: CompletionSection;
|
recommendedSection?: CompletionSection;
|
||||||
methodsSection?: CompletionSection;
|
methodsSection?: CompletionSection;
|
||||||
propSection?: CompletionSection;
|
propSection?: CompletionSection;
|
||||||
|
@ -482,12 +492,12 @@ const applySections = ({
|
||||||
{} as Record<string, Completion>,
|
{} as Record<string, Completion>,
|
||||||
);
|
);
|
||||||
return recommended
|
return recommended
|
||||||
.map(
|
.map((reco): Completion => {
|
||||||
(reco): Completion => ({
|
const option = optionByLabel[typeof reco === 'string' ? reco : reco.label];
|
||||||
...optionByLabel[reco],
|
const label =
|
||||||
section: recommendedSection,
|
typeof reco === 'string' ? option.label : insertDefaultArgs(reco.label, reco.args);
|
||||||
}),
|
return { ...option, label, section: recommendedSection };
|
||||||
)
|
})
|
||||||
.concat(
|
.concat(
|
||||||
options
|
options
|
||||||
.filter((option) => !excludeRecommended || !recommendedSet.has(option.label))
|
.filter((option) => !excludeRecommended || !recommendedSet.has(option.label))
|
||||||
|
@ -529,12 +539,12 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||||
if (validateFieldType('string', resolved, 'dateTime').valid) {
|
if (validateFieldType('string', resolved, 'dateTime').valid) {
|
||||||
return applySections({
|
return applySections({
|
||||||
options,
|
options,
|
||||||
recommended: ['toDate()'],
|
recommended: ['toDateTime()'],
|
||||||
sections: STRING_SECTIONS,
|
sections: STRING_SECTIONS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VALID_EMAIL_REGEX.test(resolved) || isUrl(resolved)) {
|
if (VALID_EMAIL_REGEX.test(resolved)) {
|
||||||
return applySections({
|
return applySections({
|
||||||
options,
|
options,
|
||||||
recommended: ['extractDomain()', 'isEmail()', ...STRING_RECOMMENDED_OPTIONS],
|
recommended: ['extractDomain()', 'isEmail()', ...STRING_RECOMMENDED_OPTIONS],
|
||||||
|
@ -542,6 +552,14 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isUrl(resolved)) {
|
||||||
|
return applySections({
|
||||||
|
options,
|
||||||
|
recommended: ['extractDomain()', 'extractUrlPath()', ...STRING_RECOMMENDED_OPTIONS],
|
||||||
|
sections: STRING_SECTIONS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (resolved.split(/\s/).find((token) => VALID_EMAIL_REGEX.test(token))) {
|
if (resolved.split(/\s/).find((token) => VALID_EMAIL_REGEX.test(token))) {
|
||||||
return applySections({
|
return applySections({
|
||||||
options,
|
options,
|
||||||
|
@ -550,6 +568,26 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trimmed = resolved.trim();
|
||||||
|
if (
|
||||||
|
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||||
|
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
||||||
|
) {
|
||||||
|
return applySections({
|
||||||
|
options,
|
||||||
|
recommended: ['parseJson()', ...STRING_RECOMMENDED_OPTIONS],
|
||||||
|
sections: STRING_SECTIONS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['true', 'false'].includes(resolved.toLocaleLowerCase())) {
|
||||||
|
return applySections({
|
||||||
|
options,
|
||||||
|
recommended: ['toBoolean()', ...STRING_RECOMMENDED_OPTIONS],
|
||||||
|
sections: STRING_SECTIONS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return applySections({
|
return applySections({
|
||||||
options,
|
options,
|
||||||
recommended: STRING_RECOMMENDED_OPTIONS,
|
recommended: STRING_RECOMMENDED_OPTIONS,
|
||||||
|
@ -557,6 +595,12 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const booleanOptions = (): Completion[] => {
|
||||||
|
return applySections({
|
||||||
|
options: sortCompletionsAlpha([...natives('boolean'), ...extensions('boolean')]),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||||
const { resolved, transformLabel } = input;
|
const { resolved, transformLabel } = input;
|
||||||
const options = sortCompletionsAlpha([
|
const options = sortCompletionsAlpha([
|
||||||
|
@ -566,6 +610,36 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||||
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
||||||
|
|
||||||
if (Number.isInteger(resolved)) {
|
if (Number.isInteger(resolved)) {
|
||||||
|
const nowMillis = Date.now();
|
||||||
|
const marginMillis = 946_707_779_000; // 30y
|
||||||
|
const isPlausableMillisDateTime =
|
||||||
|
resolved > nowMillis - marginMillis && resolved < nowMillis + marginMillis;
|
||||||
|
|
||||||
|
if (isPlausableMillisDateTime) {
|
||||||
|
return applySections({
|
||||||
|
options,
|
||||||
|
recommended: [{ label: 'toDateTime()', args: ['ms'] }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowSeconds = nowMillis / 1000;
|
||||||
|
const marginSeconds = marginMillis / 1000;
|
||||||
|
const isPlausableSecondsDateTime =
|
||||||
|
resolved > nowSeconds - marginSeconds && resolved < nowSeconds + marginSeconds;
|
||||||
|
if (isPlausableSecondsDateTime) {
|
||||||
|
return applySections({
|
||||||
|
options,
|
||||||
|
recommended: [{ label: 'toDateTime()', args: ['s'] }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved === 0 || resolved === 1) {
|
||||||
|
return applySections({
|
||||||
|
options,
|
||||||
|
recommended: ['toBoolean()'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return applySections({
|
return applySections({
|
||||||
options,
|
options,
|
||||||
recommended: ONLY_INTEGER,
|
recommended: ONLY_INTEGER,
|
||||||
|
@ -574,7 +648,7 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||||
const exclude = new Set(ONLY_INTEGER);
|
const exclude = new Set(ONLY_INTEGER);
|
||||||
return applySections({
|
return applySections({
|
||||||
options: options.filter((option) => !exclude.has(option.label)),
|
options: options.filter((option) => !exclude.has(option.label)),
|
||||||
recommended: ['round()', 'floor()', 'ceil()', 'toFixed()'],
|
recommended: ['round()', 'floor()', 'ceil()'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -775,7 +849,7 @@ const createLuxonAutocompleteOption = (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc?.hidden && !includeHidden) {
|
if (!doc || (doc?.hidden && !includeHidden)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,7 +857,11 @@ const createLuxonAutocompleteOption = (
|
||||||
label,
|
label,
|
||||||
type,
|
type,
|
||||||
section: doc?.section,
|
section: doc?.section,
|
||||||
apply: applyCompletion(hasRequiredArgs(doc), transformLabel),
|
apply: applyCompletion({
|
||||||
|
hasArgs: hasRequiredArgs(doc),
|
||||||
|
defaultArgs: getDefaultArgs(doc),
|
||||||
|
transformLabel,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
option.info = createCompletionOption(
|
option.info = createCompletionOption(
|
||||||
'DateTime',
|
'DateTime',
|
||||||
|
|
|
@ -8,11 +8,12 @@ import {
|
||||||
hasActiveNode,
|
hasActiveNode,
|
||||||
isCredentialsModalOpen,
|
isCredentialsModalOpen,
|
||||||
applyCompletion,
|
applyCompletion,
|
||||||
|
isInHttpNodePagination,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||||
import { escapeMappingString } from '@/utils/mappingUtils';
|
import { escapeMappingString } from '@/utils/mappingUtils';
|
||||||
import { PREVIOUS_NODES_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
|
import { PREVIOUS_NODES_SECTION, RECOMMENDED_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completions offered at the dollar position: `$|`
|
* Completions offered at the dollar position: `$|`
|
||||||
|
@ -48,6 +49,15 @@ export function dollarCompletions(context: CompletionContext): CompletionResult
|
||||||
|
|
||||||
export function dollarOptions(): Completion[] {
|
export function dollarOptions(): Completion[] {
|
||||||
const SKIP = new Set();
|
const SKIP = new Set();
|
||||||
|
let recommendedCompletions: Completion[] = [];
|
||||||
|
|
||||||
|
if (isInHttpNodePagination()) {
|
||||||
|
recommendedCompletions = [
|
||||||
|
{ label: '$pageCount', section: RECOMMENDED_SECTION, info: i18n.rootVars.$pageCount },
|
||||||
|
{ label: '$response', section: RECOMMENDED_SECTION, info: i18n.rootVars.$response },
|
||||||
|
{ label: '$request', section: RECOMMENDED_SECTION, info: i18n.rootVars.$request },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (isCredentialsModalOpen()) {
|
if (isCredentialsModalOpen()) {
|
||||||
return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled
|
return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled
|
||||||
|
@ -77,7 +87,9 @@ export function dollarOptions(): Completion[] {
|
||||||
section: PREVIOUS_NODES_SECTION,
|
section: PREVIOUS_NODES_SECTION,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return ROOT_DOLLAR_COMPLETIONS.filter(({ label }) => !SKIP.has(label))
|
return recommendedCompletions
|
||||||
|
.concat(ROOT_DOLLAR_COMPLETIONS)
|
||||||
|
.filter(({ label }) => !SKIP.has(label))
|
||||||
.concat(previousNodesCompletions)
|
.concat(previousNodesCompletions)
|
||||||
.map((completion) => ({ ...completion, apply: applyCompletion() }));
|
.map((completion) => ({ ...completion, apply: applyCompletion() }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { DocMetadata } from 'n8n-workflow';
|
||||||
|
|
||||||
export type Resolved = unknown;
|
export type Resolved = unknown;
|
||||||
|
|
||||||
export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object';
|
export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object' | 'boolean';
|
||||||
|
|
||||||
export type FnToDoc = { [fnName: string]: { doc?: DocMetadata } };
|
export type FnToDoc = { [fnName: string]: { doc?: DocMetadata } };
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { CREDENTIAL_EDIT_MODAL_KEY, SPLIT_IN_BATCHES_NODE_TYPE } from '@/constants';
|
import {
|
||||||
|
CREDENTIAL_EDIT_MODAL_KEY,
|
||||||
|
HTTP_REQUEST_NODE_TYPE,
|
||||||
|
SPLIT_IN_BATCHES_NODE_TYPE,
|
||||||
|
} from '@/constants';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { resolveParameter, useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { resolveParameter, useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
@ -120,6 +124,14 @@ export function hasNoParams(toResolve: string) {
|
||||||
|
|
||||||
export const isCredentialsModalOpen = () => useUIStore().modals[CREDENTIAL_EDIT_MODAL_KEY].open;
|
export const isCredentialsModalOpen = () => useUIStore().modals[CREDENTIAL_EDIT_MODAL_KEY].open;
|
||||||
|
|
||||||
|
export const isInHttpNodePagination = () => {
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
return (
|
||||||
|
ndvStore.activeNode?.type === HTTP_REQUEST_NODE_TYPE &&
|
||||||
|
ndvStore.focusedInputPath.startsWith('parameters.options.pagination')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const hasActiveNode = () => useNDVStore().activeNode?.name !== undefined;
|
export const hasActiveNode = () => useNDVStore().activeNode?.name !== undefined;
|
||||||
|
|
||||||
export const isSplitInBatchesAbsent = () =>
|
export const isSplitInBatchesAbsent = () =>
|
||||||
|
@ -151,24 +163,51 @@ export const stripExcessParens = (context: CompletionContext) => (option: Comple
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDefaultArgs = (doc?: DocMetadata): unknown[] => {
|
||||||
|
return doc?.args?.map((arg) => arg.default).filter(Boolean) ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertDefaultArgs = (label: string, args: unknown[]): string => {
|
||||||
|
if (!label.endsWith('()')) return label;
|
||||||
|
const argList = args.map((arg) => JSON.stringify(arg)).join(', ');
|
||||||
|
const fnName = label.replace('()', '');
|
||||||
|
|
||||||
|
return `${fnName}(${argList})`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a function completion is selected, set the cursor correctly
|
* When a function completion is selected, set the cursor correctly
|
||||||
|
*
|
||||||
* @example `.includes()` -> `.includes(<cursor>)`
|
* @example `.includes()` -> `.includes(<cursor>)`
|
||||||
* @example `$max()` -> `$max()<cursor>`
|
* @example `$max()` -> `$max()<cursor>`
|
||||||
*/
|
*/
|
||||||
export const applyCompletion =
|
export const applyCompletion =
|
||||||
(hasArgs = true, transform: (label: string) => string = (label) => label) =>
|
({
|
||||||
|
hasArgs = true,
|
||||||
|
defaultArgs = [],
|
||||||
|
transformLabel = (label) => label,
|
||||||
|
}: {
|
||||||
|
hasArgs?: boolean;
|
||||||
|
defaultArgs?: unknown[];
|
||||||
|
transformLabel?: (label: string) => string;
|
||||||
|
} = {}) =>
|
||||||
(view: EditorView, completion: Completion, from: number, to: number): void => {
|
(view: EditorView, completion: Completion, from: number, to: number): void => {
|
||||||
const label = transform(completion.label);
|
const isFunction = completion.label.endsWith('()');
|
||||||
|
const label = insertDefaultArgs(transformLabel(completion.label), defaultArgs);
|
||||||
|
|
||||||
const tx: TransactionSpec = {
|
const tx: TransactionSpec = {
|
||||||
...insertCompletionText(view.state, label, from, to),
|
...insertCompletionText(view.state, label, from, to),
|
||||||
annotations: pickedCompletion.of(completion),
|
annotations: pickedCompletion.of(completion),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (label.endsWith('()') && hasArgs) {
|
if (isFunction) {
|
||||||
|
if (defaultArgs.length > 0) {
|
||||||
|
tx.selection = { anchor: from + label.indexOf('(') + 1, head: from + label.length - 1 };
|
||||||
|
} else if (hasArgs) {
|
||||||
const cursorPosition = from + label.length - 1;
|
const cursorPosition = from + label.length - 1;
|
||||||
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view.dispatch(tx);
|
view.dispatch(tx);
|
||||||
};
|
};
|
||||||
|
|
|
@ -376,6 +376,9 @@ export class I18nClass {
|
||||||
$vars: this.baseText('codeNodeEditor.completer.$vars'),
|
$vars: this.baseText('codeNodeEditor.completer.$vars'),
|
||||||
$workflow: this.baseText('codeNodeEditor.completer.$workflow'),
|
$workflow: this.baseText('codeNodeEditor.completer.$workflow'),
|
||||||
DateTime: this.baseText('codeNodeEditor.completer.dateTime'),
|
DateTime: this.baseText('codeNodeEditor.completer.dateTime'),
|
||||||
|
$request: this.baseText('codeNodeEditor.completer.$request'),
|
||||||
|
$response: this.baseText('codeNodeEditor.completer.$response'),
|
||||||
|
$pageCount: this.baseText('codeNodeEditor.completer.$pageCount'),
|
||||||
} as const satisfies Record<string, string | undefined>;
|
} as const satisfies Record<string, string | undefined>;
|
||||||
|
|
||||||
proxyVars: Record<string, string | undefined> = {
|
proxyVars: Record<string, string | undefined> = {
|
||||||
|
|
|
@ -206,6 +206,9 @@
|
||||||
"codeNodeEditor.completer.$workflow.active": "Whether the workflow is active or not (boolean)",
|
"codeNodeEditor.completer.$workflow.active": "Whether the workflow is active or not (boolean)",
|
||||||
"codeNodeEditor.completer.$workflow.id": "The ID of the workflow",
|
"codeNodeEditor.completer.$workflow.id": "The ID of the workflow",
|
||||||
"codeNodeEditor.completer.$workflow.name": "The name of the workflow",
|
"codeNodeEditor.completer.$workflow.name": "The name of the workflow",
|
||||||
|
"codeNodeEditor.completer.$response": "The response object received by the HTTP node.",
|
||||||
|
"codeNodeEditor.completer.$request": "The request object sent by the HTTP node.",
|
||||||
|
"codeNodeEditor.completer.$pageCount": "Tracks how many pages the HTTP node has fetched.",
|
||||||
"codeNodeEditor.completer.dateTime": "Luxon DateTime. Use this object to parse, format and manipulate dates and times",
|
"codeNodeEditor.completer.dateTime": "Luxon DateTime. Use this object to parse, format and manipulate dates and times",
|
||||||
"codeNodeEditor.completer.binary": "The item's binary (file) data",
|
"codeNodeEditor.completer.binary": "The item's binary (file) data",
|
||||||
"codeNodeEditor.completer.globalObject.assign": "Copy of the object containing all enumerable own properties",
|
"codeNodeEditor.completer.globalObject.assign": "Copy of the object containing all enumerable own properties",
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
focusedMappableInput: '',
|
focusedMappableInput: '',
|
||||||
|
focusedInputPath: '',
|
||||||
mappingTelemetry: {},
|
mappingTelemetry: {},
|
||||||
hoveringItem: null,
|
hoveringItem: null,
|
||||||
draggable: {
|
draggable: {
|
||||||
|
@ -268,5 +269,8 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setFocusedInputPath(path: string) {
|
||||||
|
this.focusedInputPath = path;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -320,6 +320,26 @@ function intersection(value: unknown[], extraArgs: unknown[][]): unknown[] {
|
||||||
return unique(newArr, []);
|
return unique(newArr, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toJsonString(value: unknown[]) {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toInt() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFloat() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBoolean() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDateTime() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
average.doc = {
|
average.doc = {
|
||||||
name: 'average',
|
name: 'average',
|
||||||
description: 'Returns the mean average of all values in the array.',
|
description: 'Returns the mean average of all values in the array.',
|
||||||
|
@ -483,6 +503,14 @@ unique.doc = {
|
||||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-unique',
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-unique',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toJsonString.doc = {
|
||||||
|
name: 'toJsonString',
|
||||||
|
description: 'Converts an array to a JSON string',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/arrays/#array-toJsonString',
|
||||||
|
returnType: 'string',
|
||||||
|
};
|
||||||
|
|
||||||
export const arrayExtensions: ExtensionMap = {
|
export const arrayExtensions: ExtensionMap = {
|
||||||
typeName: 'Array',
|
typeName: 'Array',
|
||||||
functions: {
|
functions: {
|
||||||
|
@ -506,5 +534,10 @@ export const arrayExtensions: ExtensionMap = {
|
||||||
union,
|
union,
|
||||||
difference,
|
difference,
|
||||||
intersection,
|
intersection,
|
||||||
|
toJsonString,
|
||||||
|
toInt,
|
||||||
|
toFloat,
|
||||||
|
toBoolean,
|
||||||
|
toDateTime,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
35
packages/workflow/src/Extensions/BooleanExtensions.ts
Normal file
35
packages/workflow/src/Extensions/BooleanExtensions.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import type { ExtensionMap } from './Extensions';
|
||||||
|
|
||||||
|
export function toBoolean(value: boolean) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toInt(value: boolean) {
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFloat(value: boolean) {
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDateTime() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
toInt.doc = {
|
||||||
|
name: 'toInt',
|
||||||
|
description: 'Converts a boolean to an integer. `false` is 0, `true` is 1.',
|
||||||
|
section: 'cast',
|
||||||
|
returnType: 'boolean',
|
||||||
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/booleans/#boolean-toInt',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const booleanExtensions: ExtensionMap = {
|
||||||
|
typeName: 'Boolean',
|
||||||
|
functions: {
|
||||||
|
toBoolean,
|
||||||
|
toInt,
|
||||||
|
toFloat,
|
||||||
|
toDateTime,
|
||||||
|
},
|
||||||
|
};
|
|
@ -216,6 +216,25 @@ function plus(
|
||||||
return DateTime.fromJSDate(date).plus(duration).toJSDate();
|
return DateTime.fromJSDate(date).plus(duration).toJSDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toDateTime(date: Date | DateTime): DateTime {
|
||||||
|
if (isDateTime(date)) return date;
|
||||||
|
|
||||||
|
return DateTime.fromJSDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toInt(date: Date | DateTime): number {
|
||||||
|
if (isDateTime(date)) {
|
||||||
|
return date.toMillis();
|
||||||
|
}
|
||||||
|
return date.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toFloat = toInt;
|
||||||
|
|
||||||
|
function toBoolean() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
endOfMonth.doc = {
|
endOfMonth.doc = {
|
||||||
name: 'endOfMonth',
|
name: 'endOfMonth',
|
||||||
returnType: 'Date',
|
returnType: 'Date',
|
||||||
|
@ -267,7 +286,7 @@ format.doc = {
|
||||||
description: 'Formats a Date in the given structure.',
|
description: 'Formats a Date in the given structure.',
|
||||||
returnType: 'string',
|
returnType: 'string',
|
||||||
section: 'format',
|
section: 'format',
|
||||||
args: [{ name: 'fmt', type: 'TimeFormat' }],
|
args: [{ name: 'fmt', default: 'yyyy-MM-dd', type: 'TimeFormat' }],
|
||||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-format',
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-format',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -295,6 +314,15 @@ isInLast.doc = {
|
||||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isInLast',
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isInLast',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toDateTime.doc = {
|
||||||
|
name: 'toDateTime',
|
||||||
|
description: 'Convert a JavaScript Date to a Luxon DateTime.',
|
||||||
|
section: 'query',
|
||||||
|
returnType: 'DateTime',
|
||||||
|
hidden: true,
|
||||||
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-toDateTime',
|
||||||
|
};
|
||||||
|
|
||||||
minus.doc = {
|
minus.doc = {
|
||||||
name: 'minus',
|
name: 'minus',
|
||||||
description: 'Subtracts a given time period from a Date. Default unit is `milliseconds`.',
|
description: 'Subtracts a given time period from a Date. Default unit is `milliseconds`.',
|
||||||
|
@ -332,5 +360,9 @@ export const dateExtensions: ExtensionMap = {
|
||||||
minus,
|
minus,
|
||||||
plus,
|
plus,
|
||||||
format,
|
format,
|
||||||
|
toDateTime,
|
||||||
|
toInt,
|
||||||
|
toFloat,
|
||||||
|
toBoolean,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type { ExpressionKind } from 'ast-types/gen/kinds';
|
||||||
|
|
||||||
import type { ExpressionChunk, ExpressionCode } from './ExpressionParser';
|
import type { ExpressionChunk, ExpressionCode } from './ExpressionParser';
|
||||||
import { joinExpression, splitExpression } from './ExpressionParser';
|
import { joinExpression, splitExpression } from './ExpressionParser';
|
||||||
|
import { booleanExtensions } from './BooleanExtensions';
|
||||||
|
|
||||||
const EXPRESSION_EXTENDER = 'extend';
|
const EXPRESSION_EXTENDER = 'extend';
|
||||||
const EXPRESSION_EXTENDER_OPTIONAL = 'extendOptional';
|
const EXPRESSION_EXTENDER_OPTIONAL = 'extendOptional';
|
||||||
|
@ -33,6 +34,7 @@ export const EXTENSION_OBJECTS = [
|
||||||
numberExtensions,
|
numberExtensions,
|
||||||
objectExtensions,
|
objectExtensions,
|
||||||
stringExtensions,
|
stringExtensions,
|
||||||
|
booleanExtensions,
|
||||||
];
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
@ -48,6 +50,7 @@ const EXPRESSION_EXTENSION_METHODS = Array.from(
|
||||||
...Object.keys(dateExtensions.functions),
|
...Object.keys(dateExtensions.functions),
|
||||||
...Object.keys(arrayExtensions.functions),
|
...Object.keys(arrayExtensions.functions),
|
||||||
...Object.keys(objectExtensions.functions),
|
...Object.keys(objectExtensions.functions),
|
||||||
|
...Object.keys(booleanExtensions.functions),
|
||||||
...Object.keys(genericExtensions),
|
...Object.keys(genericExtensions),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
@ -455,7 +458,7 @@ function findExtendedFunction(input: unknown, functionName: string): FoundFuncti
|
||||||
let foundFunction: Function | undefined;
|
let foundFunction: Function | undefined;
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
foundFunction = arrayExtensions.functions[functionName];
|
foundFunction = arrayExtensions.functions[functionName];
|
||||||
} else if (isDate(input) && functionName !== 'toDate') {
|
} else if (isDate(input) && functionName !== 'toDate' && functionName !== 'toDateTime') {
|
||||||
// If it's a string date (from $json), convert it to a Date object,
|
// If it's a string date (from $json), convert it to a Date object,
|
||||||
// unless that function is `toDate`, since `toDate` does something
|
// unless that function is `toDate`, since `toDate` does something
|
||||||
// very different on date objects
|
// very different on date objects
|
||||||
|
@ -469,6 +472,8 @@ function findExtendedFunction(input: unknown, functionName: string): FoundFuncti
|
||||||
foundFunction = dateExtensions.functions[functionName];
|
foundFunction = dateExtensions.functions[functionName];
|
||||||
} else if (input !== null && typeof input === 'object') {
|
} else if (input !== null && typeof input === 'object') {
|
||||||
foundFunction = objectExtensions.functions[functionName];
|
foundFunction = objectExtensions.functions[functionName];
|
||||||
|
} else if (typeof input === 'boolean') {
|
||||||
|
foundFunction = booleanExtensions.functions[functionName];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for generic or builtin
|
// Look for generic or builtin
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
import type { ExtensionMap } from './Extensions';
|
import type { ExtensionMap } from './Extensions';
|
||||||
|
|
||||||
|
@ -40,6 +41,41 @@ function round(value: number, extraArgs: number[]) {
|
||||||
return +value.toFixed(decimalPlaces);
|
return +value.toFixed(decimalPlaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toBoolean(value: number) {
|
||||||
|
return value !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toInt(value: number) {
|
||||||
|
return round(value, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFloat(value: number) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateTimeFormat = 'ms' | 's' | 'excel';
|
||||||
|
function toDateTime(value: number, extraArgs: [DateTimeFormat]) {
|
||||||
|
const [valueFormat = 'ms'] = extraArgs;
|
||||||
|
|
||||||
|
switch (valueFormat) {
|
||||||
|
// Excel format is days since 1900
|
||||||
|
// There is a bug where 1900 is incorrectly treated as a leap year
|
||||||
|
case 'excel': {
|
||||||
|
const DAYS_BETWEEN_1900_1970 = 25567;
|
||||||
|
const DAYS_LEAP_YEAR_BUG_ADJUST = 2;
|
||||||
|
const SECONDS_IN_DAY = 86_400;
|
||||||
|
return DateTime.fromSeconds(
|
||||||
|
(value - (DAYS_BETWEEN_1900_1970 + DAYS_LEAP_YEAR_BUG_ADJUST)) * SECONDS_IN_DAY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 's':
|
||||||
|
return DateTime.fromSeconds(value);
|
||||||
|
case 'ms':
|
||||||
|
default:
|
||||||
|
return DateTime.fromMillis(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ceil.doc = {
|
ceil.doc = {
|
||||||
name: 'ceil',
|
name: 'ceil',
|
||||||
description: 'Rounds up a number to a whole number.',
|
description: 'Rounds up a number to a whole number.',
|
||||||
|
@ -89,6 +125,26 @@ round.doc = {
|
||||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-round',
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-round',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toBoolean.doc = {
|
||||||
|
name: 'toBoolean',
|
||||||
|
description: 'Converts a number to a boolean. 0 is `false`, all other numbers are `true`.',
|
||||||
|
section: 'cast',
|
||||||
|
returnType: 'boolean',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-toBoolean',
|
||||||
|
};
|
||||||
|
|
||||||
|
toDateTime.doc = {
|
||||||
|
name: 'toDateTime',
|
||||||
|
description:
|
||||||
|
"Converts a number to a DateTime. Defaults to milliseconds. Format can be 'ms' (milliseconds), 's' (seconds) or 'excel' (Excel 1900 format).",
|
||||||
|
section: 'cast',
|
||||||
|
returnType: 'DateTime',
|
||||||
|
args: [{ name: 'format?', type: 'string' }],
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/numbers/#number-toDateTime',
|
||||||
|
};
|
||||||
|
|
||||||
export const numberExtensions: ExtensionMap = {
|
export const numberExtensions: ExtensionMap = {
|
||||||
typeName: 'Number',
|
typeName: 'Number',
|
||||||
functions: {
|
functions: {
|
||||||
|
@ -98,5 +154,9 @@ export const numberExtensions: ExtensionMap = {
|
||||||
round,
|
round,
|
||||||
isEven,
|
isEven,
|
||||||
isOdd,
|
isOdd,
|
||||||
|
toBoolean,
|
||||||
|
toInt,
|
||||||
|
toFloat,
|
||||||
|
toDateTime,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -88,6 +88,26 @@ export function urlEncode(value: object) {
|
||||||
return new URLSearchParams(value as Record<string, string>).toString();
|
return new URLSearchParams(value as Record<string, string>).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toJsonString(value: object) {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toInt() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFloat() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBoolean() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDateTime() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
isEmpty.doc = {
|
isEmpty.doc = {
|
||||||
name: 'isEmpty',
|
name: 'isEmpty',
|
||||||
description: 'Checks if the Object has no key-value pairs.',
|
description: 'Checks if the Object has no key-value pairs.',
|
||||||
|
@ -168,6 +188,14 @@ values.doc = {
|
||||||
returnType: 'Array',
|
returnType: 'Array',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toJsonString.doc = {
|
||||||
|
name: 'toJsonString',
|
||||||
|
description: 'Converts an object to a JSON string',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-toJsonString',
|
||||||
|
returnType: 'string',
|
||||||
|
};
|
||||||
|
|
||||||
export const objectExtensions: ExtensionMap = {
|
export const objectExtensions: ExtensionMap = {
|
||||||
typeName: 'Object',
|
typeName: 'Object',
|
||||||
functions: {
|
functions: {
|
||||||
|
@ -181,5 +209,10 @@ export const objectExtensions: ExtensionMap = {
|
||||||
urlEncode,
|
urlEncode,
|
||||||
keys,
|
keys,
|
||||||
values,
|
values,
|
||||||
|
toJsonString,
|
||||||
|
toInt,
|
||||||
|
toFloat,
|
||||||
|
toBoolean,
|
||||||
|
toDateTime,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import SHA from 'jssha';
|
import SHA from 'jssha';
|
||||||
import MD5 from 'md5';
|
import MD5 from 'md5';
|
||||||
import { encode } from 'js-base64';
|
import { toBase64, fromBase64 } from 'js-base64';
|
||||||
import { titleCase } from 'title-case';
|
import { titleCase } from 'title-case';
|
||||||
import type { Extension, ExtensionMap } from './Extensions';
|
import type { Extension, ExtensionMap } from './Extensions';
|
||||||
import { transliterate } from 'transliteration';
|
import { transliterate } from 'transliteration';
|
||||||
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
|
import type { DateTime } from 'luxon';
|
||||||
|
import { tryToParseDateTime } from '../TypeValidation';
|
||||||
|
|
||||||
export const SupportedHashAlgorithms = [
|
export const SupportedHashAlgorithms = [
|
||||||
'md5',
|
'md5',
|
||||||
|
@ -116,7 +118,7 @@ function hash(value: string, extraArgs: string[]): string {
|
||||||
const algorithm = extraArgs[0]?.toLowerCase() ?? 'md5';
|
const algorithm = extraArgs[0]?.toLowerCase() ?? 'md5';
|
||||||
switch (algorithm) {
|
switch (algorithm) {
|
||||||
case 'base64':
|
case 'base64':
|
||||||
return encode(value);
|
return toBase64(value);
|
||||||
case 'md5':
|
case 'md5':
|
||||||
return MD5(value);
|
return MD5(value);
|
||||||
case 'sha1':
|
case 'sha1':
|
||||||
|
@ -214,6 +216,14 @@ function toDate(value: string): Date {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toDateTime(value: string): DateTime {
|
||||||
|
try {
|
||||||
|
return tryToParseDateTime(value);
|
||||||
|
} catch (error) {
|
||||||
|
throw new ExpressionExtensionError('cannot convert to Luxon DateTime');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function urlDecode(value: string, extraArgs: boolean[]): string {
|
function urlDecode(value: string, extraArgs: boolean[]): string {
|
||||||
const [entireString = false] = extraArgs;
|
const [entireString = false] = extraArgs;
|
||||||
if (entireString) {
|
if (entireString) {
|
||||||
|
@ -359,6 +369,37 @@ function extractUrl(value: string) {
|
||||||
return matched[0];
|
return matched[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractUrlPath(value: string) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.pathname;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(value: string): unknown {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBoolean(value: string): boolean {
|
||||||
|
const normalized = value.toLowerCase();
|
||||||
|
const FALSY = new Set(['false', 'no', '0']);
|
||||||
|
return normalized.length > 0 && !FALSY.has(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64Encode(value: string): string {
|
||||||
|
return toBase64(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64Decode(value: string): string {
|
||||||
|
return fromBase64(value);
|
||||||
|
}
|
||||||
|
|
||||||
removeMarkdown.doc = {
|
removeMarkdown.doc = {
|
||||||
name: 'removeMarkdown',
|
name: 'removeMarkdown',
|
||||||
description: 'Removes Markdown formatting from a string.',
|
description: 'Removes Markdown formatting from a string.',
|
||||||
|
@ -382,9 +423,28 @@ toDate.doc = {
|
||||||
description: 'Converts a string to a date.',
|
description: 'Converts a string to a date.',
|
||||||
section: 'cast',
|
section: 'cast',
|
||||||
returnType: 'Date',
|
returnType: 'Date',
|
||||||
|
hidden: true,
|
||||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toDate',
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toDate',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toDateTime.doc = {
|
||||||
|
name: 'toDateTime',
|
||||||
|
description: 'Converts a string to a Luxon DateTime.',
|
||||||
|
section: 'cast',
|
||||||
|
returnType: 'DateTime',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toDateTime',
|
||||||
|
};
|
||||||
|
|
||||||
|
toBoolean.doc = {
|
||||||
|
name: 'toBoolean',
|
||||||
|
description: 'Converts a string to a boolean.',
|
||||||
|
section: 'cast',
|
||||||
|
returnType: 'boolean',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toBoolean',
|
||||||
|
};
|
||||||
|
|
||||||
toFloat.doc = {
|
toFloat.doc = {
|
||||||
name: 'toFloat',
|
name: 'toFloat',
|
||||||
description: 'Converts a string to a decimal number.',
|
description: 'Converts a string to a decimal number.',
|
||||||
|
@ -549,6 +609,15 @@ extractUrl.doc = {
|
||||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractUrl',
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractUrl',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extractUrlPath.doc = {
|
||||||
|
name: 'extractUrlPath',
|
||||||
|
description: 'Extracts the path from a URL. Returns undefined if none is found.',
|
||||||
|
section: 'edit',
|
||||||
|
returnType: 'string',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractUrlPath',
|
||||||
|
};
|
||||||
|
|
||||||
hash.doc = {
|
hash.doc = {
|
||||||
name: 'hash',
|
name: 'hash',
|
||||||
description: 'Returns a string hashed with the given algorithm. Default algorithm is `md5`.',
|
description: 'Returns a string hashed with the given algorithm. Default algorithm is `md5`.',
|
||||||
|
@ -567,6 +636,34 @@ quote.doc = {
|
||||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-quote',
|
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-quote',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
parseJson.doc = {
|
||||||
|
name: 'parseJson',
|
||||||
|
description:
|
||||||
|
'Parses a JSON string, constructing the JavaScript value or object described by the string.',
|
||||||
|
section: 'cast',
|
||||||
|
returnType: 'any',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-parseJson',
|
||||||
|
};
|
||||||
|
|
||||||
|
base64Encode.doc = {
|
||||||
|
name: 'base64Encode',
|
||||||
|
description: 'Converts a UTF-8-encoded string to a Base64 string.',
|
||||||
|
section: 'edit',
|
||||||
|
returnType: 'string',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-base64Encode',
|
||||||
|
};
|
||||||
|
|
||||||
|
base64Decode.doc = {
|
||||||
|
name: 'base64Decode',
|
||||||
|
description: 'Converts a Base64 string to a UTF-8 string.',
|
||||||
|
section: 'edit',
|
||||||
|
returnType: 'string',
|
||||||
|
docURL:
|
||||||
|
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-base64Decode',
|
||||||
|
};
|
||||||
|
|
||||||
const toDecimalNumber: Extension = toFloat.bind({});
|
const toDecimalNumber: Extension = toFloat.bind({});
|
||||||
toDecimalNumber.doc = { ...toFloat.doc, hidden: true };
|
toDecimalNumber.doc = { ...toFloat.doc, hidden: true };
|
||||||
const toWholeNumber: Extension = toInt.bind({});
|
const toWholeNumber: Extension = toInt.bind({});
|
||||||
|
@ -579,6 +676,8 @@ export const stringExtensions: ExtensionMap = {
|
||||||
removeMarkdown,
|
removeMarkdown,
|
||||||
removeTags,
|
removeTags,
|
||||||
toDate,
|
toDate,
|
||||||
|
toDateTime,
|
||||||
|
toBoolean,
|
||||||
toDecimalNumber,
|
toDecimalNumber,
|
||||||
toFloat,
|
toFloat,
|
||||||
toInt,
|
toInt,
|
||||||
|
@ -600,5 +699,9 @@ export const stringExtensions: ExtensionMap = {
|
||||||
extractEmail,
|
extractEmail,
|
||||||
extractDomain,
|
extractDomain,
|
||||||
extractUrl,
|
extractUrl,
|
||||||
|
extractUrlPath,
|
||||||
|
parseJson,
|
||||||
|
base64Encode,
|
||||||
|
base64Decode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
16
packages/workflow/src/NativeMethods/Boolean.methods.ts
Normal file
16
packages/workflow/src/NativeMethods/Boolean.methods.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type { NativeDoc } from '@/Extensions/Extensions';
|
||||||
|
|
||||||
|
export const booleanMethods: NativeDoc = {
|
||||||
|
typeName: 'Boolean',
|
||||||
|
functions: {
|
||||||
|
toString: {
|
||||||
|
doc: {
|
||||||
|
name: 'toString',
|
||||||
|
description: 'returns a string representing this boolean value.',
|
||||||
|
docURL:
|
||||||
|
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/toString',
|
||||||
|
returnType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ export const numberMethods: NativeDoc = {
|
||||||
toFixed: {
|
toFixed: {
|
||||||
doc: {
|
doc: {
|
||||||
name: 'toFixed',
|
name: 'toFixed',
|
||||||
|
hidden: true,
|
||||||
description:
|
description:
|
||||||
'Formats a number using fixed-point notation. `digits` defaults to null if not given.',
|
'Formats a number using fixed-point notation. `digits` defaults to null if not given.',
|
||||||
docURL:
|
docURL:
|
||||||
|
@ -17,6 +18,7 @@ export const numberMethods: NativeDoc = {
|
||||||
toPrecision: {
|
toPrecision: {
|
||||||
doc: {
|
doc: {
|
||||||
name: 'toPrecision',
|
name: 'toPrecision',
|
||||||
|
hidden: true,
|
||||||
description: 'Returns a string representing the number to the specified precision.',
|
description: 'Returns a string representing the number to the specified precision.',
|
||||||
docURL:
|
docURL:
|
||||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision',
|
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision',
|
||||||
|
@ -24,5 +26,14 @@ export const numberMethods: NativeDoc = {
|
||||||
args: [{ name: 'precision?', type: 'number' }],
|
args: [{ name: 'precision?', type: 'number' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
toString: {
|
||||||
|
doc: {
|
||||||
|
name: 'toString',
|
||||||
|
description: 'returns a string representing this number value.',
|
||||||
|
docURL:
|
||||||
|
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString',
|
||||||
|
returnType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,14 @@ import { arrayMethods } from './Array.methods';
|
||||||
import { numberMethods } from './Number.methods';
|
import { numberMethods } from './Number.methods';
|
||||||
import { objectMethods } from './Object.Methods';
|
import { objectMethods } from './Object.Methods';
|
||||||
import type { NativeDoc } from '@/Extensions/Extensions';
|
import type { NativeDoc } from '@/Extensions/Extensions';
|
||||||
|
import { booleanMethods } from './Boolean.methods';
|
||||||
|
|
||||||
const NATIVE_METHODS: NativeDoc[] = [stringMethods, arrayMethods, numberMethods, objectMethods];
|
const NATIVE_METHODS: NativeDoc[] = [
|
||||||
|
stringMethods,
|
||||||
|
arrayMethods,
|
||||||
|
numberMethods,
|
||||||
|
objectMethods,
|
||||||
|
booleanMethods,
|
||||||
|
];
|
||||||
|
|
||||||
export { NATIVE_METHODS as NativeMethods };
|
export { NATIVE_METHODS as NativeMethods };
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { arrayExtensions } from '../../src/Extensions/ArrayExtensions';
|
||||||
import { evaluate } from './Helpers';
|
import { evaluate } from './Helpers';
|
||||||
|
|
||||||
describe('Data Transformation Functions', () => {
|
describe('Data Transformation Functions', () => {
|
||||||
|
@ -234,5 +235,27 @@ describe('Data Transformation Functions', () => {
|
||||||
[16, 17, 18, 19, 20],
|
[16, 17, 18, 19, 20],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('.toJsonString() should work on an array', () => {
|
||||||
|
expect(evaluate('={{ [true, 1, "one", {foo: "bar"}].toJsonString() }}')).toEqual(
|
||||||
|
'[true,1,"one",{"foo":"bar"}]',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Conversion methods', () => {
|
||||||
|
test('should exist but return undefined (to not break expressions with mixed data)', () => {
|
||||||
|
expect(evaluate('={{ numberList(1, 20).toInt() }}')).toBeUndefined();
|
||||||
|
expect(evaluate('={{ numberList(1, 20).toFloat() }}')).toBeUndefined();
|
||||||
|
expect(evaluate('={{ numberList(1, 20).toBoolean() }}')).toBeUndefined();
|
||||||
|
expect(evaluate('={{ numberList(1, 20).toDateTime() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have a doc (hidden from autocomplete)', () => {
|
||||||
|
expect(arrayExtensions.functions.toInt.doc).toBeUndefined();
|
||||||
|
expect(arrayExtensions.functions.toFloat.doc).toBeUndefined();
|
||||||
|
expect(arrayExtensions.functions.toBoolean.doc).toBeUndefined();
|
||||||
|
expect(arrayExtensions.functions.toDateTime.doc).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { booleanExtensions } from '../../src/Extensions/BooleanExtensions';
|
||||||
|
import { evaluate } from './Helpers';
|
||||||
|
|
||||||
|
describe('Data Transformation Functions', () => {
|
||||||
|
describe('Boolean Data Transformation Functions', () => {
|
||||||
|
describe('Conversion methods', () => {
|
||||||
|
describe('toInt/toFloat', () => {
|
||||||
|
test('should return 1 for true, 0 for false', () => {
|
||||||
|
expect(evaluate('={{ (true).toInt() }}')).toEqual(1);
|
||||||
|
expect(evaluate('={{ (true).toFloat() }}')).toEqual(1);
|
||||||
|
expect(evaluate('={{ (false).toInt() }}')).toEqual(0);
|
||||||
|
expect(evaluate('={{ (false).toFloat() }}')).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toDateTime', () => {
|
||||||
|
test('should return undefined', () => {
|
||||||
|
expect(evaluate('={{ (true).toDateTime() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toBoolean', () => {
|
||||||
|
test('should return itself', () => {
|
||||||
|
expect(evaluate('={{ (true).toDateTime() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have a doc (hidden from autocomplete)', () => {
|
||||||
|
expect(booleanExtensions.functions.toFloat.doc).toBeUndefined();
|
||||||
|
expect(booleanExtensions.functions.toBoolean.doc).toBeUndefined();
|
||||||
|
expect(booleanExtensions.functions.toDateTime.doc).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,6 +5,7 @@
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { getGlobalState } from '@/GlobalState';
|
import { getGlobalState } from '@/GlobalState';
|
||||||
import { evaluate, getLocalISOString } from './Helpers';
|
import { evaluate, getLocalISOString } from './Helpers';
|
||||||
|
import { dateExtensions } from '../../src/Extensions/DateExtensions';
|
||||||
|
|
||||||
const { defaultTimezone } = getGlobalState();
|
const { defaultTimezone } = getGlobalState();
|
||||||
|
|
||||||
|
@ -107,5 +108,56 @@ describe('Data Transformation Functions', () => {
|
||||||
evaluate("={{ $now.isBetween($now, '2023-06-23', '2023-09-21'.toDate()) }}"),
|
evaluate("={{ $now.isBetween($now, '2023-06-23', '2023-09-21'.toDate()) }}"),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('toDateTime', () => {
|
||||||
|
test('should return itself for DateTime', () => {
|
||||||
|
const result = evaluate(
|
||||||
|
"={{ DateTime.fromFormat('01-01-2024', 'dd-MM-yyyy').toDateTime() }}",
|
||||||
|
) as unknown as DateTime;
|
||||||
|
expect(result).toBeInstanceOf(DateTime);
|
||||||
|
expect(result.day).toEqual(1);
|
||||||
|
expect(result.month).toEqual(1);
|
||||||
|
expect(result.year).toEqual(2024);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return a DateTime for JS Date', () => {
|
||||||
|
const result = evaluate(
|
||||||
|
'={{ new Date(2024, 0, 1, 12).toDateTime() }}',
|
||||||
|
) as unknown as DateTime;
|
||||||
|
expect(result).toBeInstanceOf(DateTime);
|
||||||
|
expect(result.day).toEqual(1);
|
||||||
|
expect(result.month).toEqual(1);
|
||||||
|
expect(result.year).toEqual(2024);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toInt/toFloat', () => {
|
||||||
|
test('should return milliseconds for DateTime', () => {
|
||||||
|
expect(evaluate("={{ DateTime.fromISO('2024-01-01T00:00:00.000Z').toInt() }}")).toEqual(
|
||||||
|
1704067200000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return milliseconds for JS Date', () => {
|
||||||
|
expect(evaluate('={{ new Date("2024-01-01T00:00:00.000Z").toFloat() }}')).toEqual(
|
||||||
|
1704067200000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have a doc (hidden from autocomplete)', () => {
|
||||||
|
expect(dateExtensions.functions.toInt.doc).toBeUndefined();
|
||||||
|
expect(dateExtensions.functions.toFloat.doc).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toBoolean', () => {
|
||||||
|
test('should return undefined', () => {
|
||||||
|
expect(evaluate('={{ new Date("2024-01-01T00:00:00.000Z").toBoolean() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have a doc (hidden from autocomplete)', () => {
|
||||||
|
expect(dateExtensions.functions.toBoolean.doc).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,6 +55,52 @@ describe('Data Transformation Functions', () => {
|
||||||
expect(() => evaluate('={{ (NaN).isEven() }}')).toThrow();
|
expect(() => evaluate('={{ (NaN).isEven() }}')).toThrow();
|
||||||
expect(() => evaluate('={{ (9.2).isEven() }}')).toThrow();
|
expect(() => evaluate('={{ (9.2).isEven() }}')).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('toDateTime', () => {
|
||||||
|
test('from milliseconds (default)', () => {
|
||||||
|
expect(evaluate('={{ (1704085200000).toDateTime().toISO() }}')).toEqual(
|
||||||
|
'2024-01-01T00:00:00.000-05:00',
|
||||||
|
);
|
||||||
|
expect(evaluate('={{ (1704085200000).toDateTime("ms").toISO() }}')).toEqual(
|
||||||
|
'2024-01-01T00:00:00.000-05:00',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('from seconds', () => {
|
||||||
|
expect(evaluate('={{ (1704085200).toDateTime("s").toISO() }}')).toEqual(
|
||||||
|
'2024-01-01T00:00:00.000-05:00',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('from Excel 1900 format', () => {
|
||||||
|
expect(evaluate('={{ (42144).toDateTime("excel").toISO() }}')).toEqual(
|
||||||
|
'2015-05-19T20:00:00.000-04:00',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toInt', () => {
|
||||||
|
test('should round numbers', () => {
|
||||||
|
expect(evaluate('={{ (42144).toInt() }}')).toEqual(42144);
|
||||||
|
expect(evaluate('={{ (42144.345).toInt() }}')).toEqual(42144);
|
||||||
|
expect(evaluate('={{ (42144.545).toInt() }}')).toEqual(42145);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toFloat', () => {
|
||||||
|
test('should return itself', () => {
|
||||||
|
expect(evaluate('={{ (42144).toFloat() }}')).toEqual(42144);
|
||||||
|
expect(evaluate('={{ (42144.345).toFloat() }}')).toEqual(42144.345);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toBoolean', () => {
|
||||||
|
test('should return false for 0, 1 for other numbers', () => {
|
||||||
|
expect(evaluate('={{ (42144).toBoolean() }}')).toBe(true);
|
||||||
|
expect(evaluate('={{ (-1.549).toBoolean() }}')).toBe(true);
|
||||||
|
expect(evaluate('={{ (0).toBoolean() }}')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Multiple expressions', () => {
|
describe('Multiple expressions', () => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { objectExtensions } from '../../src/Extensions/ObjectExtensions';
|
||||||
import { evaluate } from './Helpers';
|
import { evaluate } from './Helpers';
|
||||||
|
|
||||||
describe('Data Transformation Functions', () => {
|
describe('Data Transformation Functions', () => {
|
||||||
|
@ -89,5 +90,27 @@ describe('Data Transformation Functions', () => {
|
||||||
test('.values should work on an object', () => {
|
test('.values should work on an object', () => {
|
||||||
expect(evaluate('={{ ({ test1: 1, test2: "2" }).values() }}')).toEqual([1, '2']);
|
expect(evaluate('={{ ({ test1: 1, test2: "2" }).values() }}')).toEqual([1, '2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('.toJsonString() should work on an object', () => {
|
||||||
|
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toJsonString() }}')).toEqual(
|
||||||
|
'{"test1":1,"test2":"2"}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Conversion methods', () => {
|
||||||
|
test('should exist but return undefined (to not break expressions with mixed data)', () => {
|
||||||
|
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toInt() }}')).toBeUndefined();
|
||||||
|
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toFloat() }}')).toBeUndefined();
|
||||||
|
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toBoolean() }}')).toBeUndefined();
|
||||||
|
expect(evaluate('={{ ({ test1: 1, test2: "2" }).toDateTime() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have a doc (hidden from autocomplete)', () => {
|
||||||
|
expect(objectExtensions.functions.toInt.doc).toBeUndefined();
|
||||||
|
expect(objectExtensions.functions.toFloat.doc).toBeUndefined();
|
||||||
|
expect(objectExtensions.functions.toBoolean.doc).toBeUndefined();
|
||||||
|
expect(objectExtensions.functions.toDateTime.doc).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { evaluate } from './Helpers';
|
import { evaluate } from './Helpers';
|
||||||
|
import { ExpressionExtensionError } from '../../src/errors';
|
||||||
|
|
||||||
describe('Data Transformation Functions', () => {
|
describe('Data Transformation Functions', () => {
|
||||||
describe('String Data Transformation Functions', () => {
|
describe('String Data Transformation Functions', () => {
|
||||||
|
@ -244,5 +246,48 @@ describe('Data Transformation Functions', () => {
|
||||||
expect(evaluate('={{ "aaaaaaaa".isEmail() }}')).toEqual(false);
|
expect(evaluate('={{ "aaaaaaaa".isEmail() }}')).toEqual(false);
|
||||||
expect(evaluate('={{ "test @ n8n".isEmail() }}')).toEqual(false);
|
expect(evaluate('={{ "test @ n8n".isEmail() }}')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('.toDateTime should work on a variety of formats', () => {
|
||||||
|
expect(evaluate('={{ "Wed, 21 Oct 2015 07:28:00 GMT".toDateTime() }}')).toBeInstanceOf(
|
||||||
|
DateTime,
|
||||||
|
);
|
||||||
|
expect(evaluate('={{ "2008-11-11".toDateTime() }}')).toBeInstanceOf(DateTime);
|
||||||
|
expect(evaluate('={{ "1-Feb-2024".toDateTime() }}')).toBeInstanceOf(DateTime);
|
||||||
|
expect(() => evaluate('={{ "hi".toDateTime() }}')).toThrowError(
|
||||||
|
new ExpressionExtensionError('cannot convert to Luxon DateTime'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('.extractUrlPath should work on a string', () => {
|
||||||
|
expect(
|
||||||
|
evaluate('={{ "https://example.com/orders/1/detail#hash?foo=bar".extractUrlPath() }}'),
|
||||||
|
).toEqual('/orders/1/detail');
|
||||||
|
expect(evaluate('={{ "hi".extractUrlPath() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('.parseJson should work on a string', () => {
|
||||||
|
expect(evaluate('={{ \'{"test1":1,"test2":"2"}\'.parseJson() }}')).toEqual({
|
||||||
|
test1: 1,
|
||||||
|
test2: '2',
|
||||||
|
});
|
||||||
|
expect(evaluate('={{ "hi".parseJson() }}')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('.toBoolean should work on a string', () => {
|
||||||
|
expect(evaluate('={{ "False".toBoolean() }}')).toBe(false);
|
||||||
|
expect(evaluate('={{ "".toBoolean() }}')).toBe(false);
|
||||||
|
expect(evaluate('={{ "0".toBoolean() }}')).toBe(false);
|
||||||
|
expect(evaluate('={{ "no".toBoolean() }}')).toBe(false);
|
||||||
|
expect(evaluate('={{ "TRUE".toBoolean() }}')).toBe(true);
|
||||||
|
expect(evaluate('={{ "hello".toBoolean() }}')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('.base64Encode should work on a string', () => {
|
||||||
|
expect(evaluate('={{ "n8n test".base64Encode() }}')).toBe('bjhuIHRlc3Q=');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('.base64Decode should work on a string', () => {
|
||||||
|
expect(evaluate('={{ "bjhuIHRlc3Q=".base64Decode() }}')).toBe('n8n test');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue