mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
feat: Recovery option for jsonParse helper (#10182)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
1718125c6d
commit
d165b33cea
|
@ -2,6 +2,7 @@ import { DateTime } from 'luxon';
|
|||
import type { FieldType, INodePropertyOptions, ValidationResult } from './Interfaces';
|
||||
import isObject from 'lodash/isObject';
|
||||
import { ApplicationError } from './errors';
|
||||
import { jsonParse } from './utils';
|
||||
|
||||
export const tryToParseNumber = (value: unknown): number => {
|
||||
const isValidNumber = !isNaN(Number(value));
|
||||
|
@ -135,7 +136,8 @@ export const tryToParseObject = (value: unknown): object => {
|
|||
return value;
|
||||
}
|
||||
try {
|
||||
const o = JSON.parse(String(value)) as object;
|
||||
const o = jsonParse<object>(String(value), { acceptJSObject: true });
|
||||
|
||||
if (typeof o !== 'object' || Array.isArray(o)) {
|
||||
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
||||
}
|
||||
|
|
|
@ -5,6 +5,13 @@ import { ALPHABET } from './Constants';
|
|||
import type { BinaryFileType, IDisplayOptions, INodeProperties, JsonObject } from './Interfaces';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
import {
|
||||
parse as esprimaParse,
|
||||
Syntax,
|
||||
type Node as SyntaxNode,
|
||||
type ExpressionStatement,
|
||||
} from 'esprima-next';
|
||||
|
||||
const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']);
|
||||
|
||||
// NOTE: BigInt.prototype.toJSON is not available, which causes JSON.stringify to throw an error
|
||||
|
@ -68,17 +75,74 @@ export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string })
|
|||
};
|
||||
// eslint-enable
|
||||
|
||||
function syntaxNodeToValue(expression?: SyntaxNode | null): unknown {
|
||||
switch (expression?.type) {
|
||||
case Syntax.ObjectExpression:
|
||||
return Object.fromEntries(
|
||||
expression.properties
|
||||
.filter((prop) => prop.type === Syntax.Property)
|
||||
.map(({ key, value }) => [syntaxNodeToValue(key), syntaxNodeToValue(value)]),
|
||||
);
|
||||
case Syntax.Identifier:
|
||||
return expression.name;
|
||||
case Syntax.Literal:
|
||||
return expression.value;
|
||||
case Syntax.ArrayExpression:
|
||||
return expression.elements.map((exp) => syntaxNodeToValue(exp));
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse any JavaScript ObjectExpression, including:
|
||||
* - single quoted keys
|
||||
* - unquoted keys
|
||||
*/
|
||||
function parseJSObject(objectAsString: string): object {
|
||||
const jsExpression = esprimaParse(`(${objectAsString})`).body.find(
|
||||
(node): node is ExpressionStatement =>
|
||||
node.type === Syntax.ExpressionStatement && node.expression.type === Syntax.ObjectExpression,
|
||||
);
|
||||
|
||||
return syntaxNodeToValue(jsExpression?.expression) as object;
|
||||
}
|
||||
|
||||
type MutuallyExclusive<T, U> =
|
||||
| (T & { [k in Exclude<keyof U, keyof T>]?: never })
|
||||
| (U & { [k in Exclude<keyof T, keyof U>]?: never });
|
||||
|
||||
type JSONParseOptions<T> = MutuallyExclusive<{ errorMessage: string }, { fallbackValue: T }>;
|
||||
type JSONParseOptions<T> = { acceptJSObject?: boolean } & MutuallyExclusive<
|
||||
{ errorMessage?: string },
|
||||
{ fallbackValue?: T }
|
||||
>;
|
||||
|
||||
/**
|
||||
* Parses a JSON string into an object with optional error handling and recovery mechanisms.
|
||||
*
|
||||
* @param {string} jsonString - The JSON string to parse.
|
||||
* @param {Object} [options] - Optional settings for parsing the JSON string. Either `fallbackValue` or `errorMessage` can be set, but not both.
|
||||
* @param {boolean} [options.parseJSObject=false] - If true, attempts to recover from common JSON format errors by parsing the JSON string as a JavaScript Object.
|
||||
* @param {string} [options.errorMessage] - A custom error message to throw if the JSON string cannot be parsed.
|
||||
* @param {*} [options.fallbackValue] - A fallback value to return if the JSON string cannot be parsed.
|
||||
* @returns {Object} - The parsed object, or the fallback value if parsing fails and `fallbackValue` is set.
|
||||
*/
|
||||
export const jsonParse = <T>(jsonString: string, options?: JSONParseOptions<T>): T => {
|
||||
try {
|
||||
return JSON.parse(jsonString) as T;
|
||||
} catch (error) {
|
||||
if (options?.acceptJSObject) {
|
||||
try {
|
||||
const jsonStringCleaned = parseJSObject(jsonString);
|
||||
return jsonStringCleaned as T;
|
||||
} catch (e) {
|
||||
// Ignore this error and return the original error or the fallback value
|
||||
}
|
||||
}
|
||||
if (options?.fallbackValue !== undefined) {
|
||||
if (options.fallbackValue instanceof Function) {
|
||||
return options.fallbackValue();
|
||||
}
|
||||
return options.fallbackValue;
|
||||
} else if (options?.errorMessage) {
|
||||
throw new ApplicationError(options.errorMessage);
|
||||
|
|
|
@ -145,10 +145,14 @@ describe('Type Validation', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should validate and cast JSON properly', () => {
|
||||
it('should validate and cast JSON & JS objects properly', () => {
|
||||
const VALID_OBJECTS = [
|
||||
['{"a": 1}', { a: 1 }],
|
||||
['{a: 1}', { a: 1 }],
|
||||
["{'a': '1'}", { a: '1' }],
|
||||
["{'\\'single quoted\\' \"double quoted\"': 1}", { '\'single quoted\' "double quoted"': 1 }],
|
||||
['{"a": 1, "b": { "c": 10, "d": "test"}}', { a: 1, b: { c: 10, d: 'test' } }],
|
||||
["{\"a\": 1, b: { 'c': 10, d: 'test'}}", { a: 1, b: { c: 10, d: 'test' } }],
|
||||
[{ name: 'John' }, { name: 'John' }],
|
||||
[
|
||||
{ name: 'John', address: { street: 'Via Roma', city: 'Milano' } },
|
||||
|
@ -162,19 +166,18 @@ describe('Type Validation', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
const INVALID_JSON = [
|
||||
const INVALID_OBJECTS = [
|
||||
['one', 'two'],
|
||||
'1',
|
||||
'[1]',
|
||||
'1.1',
|
||||
1.1,
|
||||
'"a"',
|
||||
'{a: 1}',
|
||||
'["apples", "oranges"]',
|
||||
[{ name: 'john' }, { name: 'bob' }],
|
||||
'[ { name: "john" }, { name: "bob" } ]',
|
||||
];
|
||||
INVALID_JSON.forEach((value) =>
|
||||
INVALID_OBJECTS.forEach((value) =>
|
||||
expect(validateFieldType('json', value, 'object').valid).toEqual(false),
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue