mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
fix(Form Node): Improve error messages for invalid form definition JSON (#37291)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6cc8c59f5a
commit
d8ce73c1df
|
|
@ -203,69 +203,83 @@ const ALLOWED_FIELD_TYPES = [
|
|||
'hiddenField',
|
||||
];
|
||||
|
||||
const tryToParseFormFieldOptions = (rawOptions: unknown, index: number): object => {
|
||||
const asObject = Array.isArray(rawOptions) ? { values: rawOptions } : rawOptions;
|
||||
|
||||
const invalidValuesError = new UserError(
|
||||
`Field dropdown in field ${index} has no 'values' property that contains an array of options`,
|
||||
);
|
||||
|
||||
if (typeof asObject !== 'object' || asObject === null) throw invalidValuesError;
|
||||
|
||||
const { values } = asObject as { [key: string]: unknown };
|
||||
if (!Array.isArray(values)) throw invalidValuesError;
|
||||
|
||||
const options: unknown[] = values;
|
||||
for (const [optionIndex, option] of options.entries()) {
|
||||
if (
|
||||
typeof option !== 'object' ||
|
||||
option === null ||
|
||||
Object.keys(option).length !== 1 ||
|
||||
typeof (option as { option?: unknown }).option !== 'string'
|
||||
) {
|
||||
throw new UserError(`Field dropdown in field ${index} has an invalid option ${optionIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
return asObject;
|
||||
};
|
||||
|
||||
export const tryToParseJsonToFormFields = (value: unknown): FormFieldsParameter => {
|
||||
const fields: FormFieldsParameter = [];
|
||||
|
||||
let rawFields: Array<{ [key: string]: unknown }>;
|
||||
try {
|
||||
const rawFields = jsonParse<Array<{ [key: string]: unknown }>>(value as string, {
|
||||
rawFields = jsonParse<Array<{ [key: string]: unknown }>>(value as string, {
|
||||
acceptJSObject: true,
|
||||
});
|
||||
} catch (error) {
|
||||
// Keep the engine message, it contains the error position
|
||||
throw new UserError(`Value is not valid JSON: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
for (const [index, field] of rawFields.entries()) {
|
||||
for (const key of Object.keys(field)) {
|
||||
if (!ALLOWED_FORM_FIELDS_KEYS.includes(key)) {
|
||||
throw new UserError(`Key '${key}' in field ${index} is not valid for form fields`);
|
||||
}
|
||||
if (
|
||||
key !== 'fieldOptions' &&
|
||||
!['string', 'number', 'boolean'].includes(typeof field[key])
|
||||
) {
|
||||
field[key] = String(field[key]);
|
||||
} else if (typeof field[key] === 'string' && key !== 'html') {
|
||||
field[key] = field[key].replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
if (!Array.isArray(rawFields)) {
|
||||
throw new UserError(
|
||||
`Form fields must be an array of objects, but we got ${getValueDescription(rawFields)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'fieldType' && !ALLOWED_FIELD_TYPES.includes(field[key] as string)) {
|
||||
throw new UserError(
|
||||
`Field type '${field[key] as string}' in field ${index} is not valid for form fields`,
|
||||
);
|
||||
}
|
||||
for (const [index, field] of rawFields.entries()) {
|
||||
if (typeof field !== 'object' || field === null || Array.isArray(field)) {
|
||||
throw new UserError(
|
||||
`Field ${index} must be an object, but we got ${getValueDescription(field)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'fieldOptions') {
|
||||
if (Array.isArray(field[key])) {
|
||||
field[key] = { values: field[key] };
|
||||
}
|
||||
|
||||
if (
|
||||
typeof field[key] !== 'object' ||
|
||||
!(field[key] as { [key: string]: unknown }).values
|
||||
) {
|
||||
throw new UserError(
|
||||
`Field dropdown in field ${index} does has no 'values' property that contain an array of options`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [optionIndex, option] of (
|
||||
(field[key] as { [key: string]: unknown }).values as Array<{
|
||||
[key: string]: { option: string };
|
||||
}>
|
||||
).entries()) {
|
||||
if (Object.keys(option).length !== 1 || typeof option.option !== 'string') {
|
||||
throw new UserError(
|
||||
`Field dropdown in field ${index} has an invalid option ${optionIndex}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(field)) {
|
||||
if (!ALLOWED_FORM_FIELDS_KEYS.includes(key)) {
|
||||
throw new UserError(`Key '${key}' in field ${index} is not valid for form fields`);
|
||||
}
|
||||
if (key !== 'fieldOptions' && !['string', 'number', 'boolean'].includes(typeof field[key])) {
|
||||
field[key] = String(field[key]);
|
||||
} else if (typeof field[key] === 'string' && key !== 'html') {
|
||||
field[key] = field[key].replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
fields.push(field as FormFieldsParameter[number]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationError || error instanceof BaseError) throw error;
|
||||
if (key === 'fieldType' && !ALLOWED_FIELD_TYPES.includes(field[key] as string)) {
|
||||
throw new UserError(
|
||||
`Field type '${field[key] as string}' in field ${index} is not valid for form fields`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new UserError('Value is not valid JSON');
|
||||
if (key === 'fieldOptions') {
|
||||
field[key] = tryToParseFormFieldOptions(field[key], index);
|
||||
}
|
||||
}
|
||||
|
||||
fields.push(field as FormFieldsParameter[number]);
|
||||
}
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -372,6 +372,41 @@ describe('Type Validation', () => {
|
|||
const fields = tryToParseJsonToFormFields(json);
|
||||
expect(fields).toEqual([{ fieldType: 'html', html: '<div>test</div>' }]);
|
||||
});
|
||||
|
||||
it('should include the parse error detail for invalid JSON', () => {
|
||||
const json = '[{"fieldLabel": }]';
|
||||
expect(() => tryToParseJsonToFormFields(json)).toThrowError(/^Value is not valid JSON: .+/);
|
||||
});
|
||||
|
||||
it('should reject valid JSON that is not an array', () => {
|
||||
const json = '{"fieldLabel": "Name"}';
|
||||
expect(() => tryToParseJsonToFormFields(json)).toThrowError(
|
||||
'Form fields must be an array of objects, but we got object',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject array entries that are not objects', () => {
|
||||
const json = '[null]';
|
||||
expect(() => tryToParseJsonToFormFields(json)).toThrowError(
|
||||
"Field 0 must be an object, but we got 'null'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject fieldOptions without a values array', () => {
|
||||
const json =
|
||||
'[{"fieldLabel": "Choice", "fieldType": "dropdown", "fieldOptions": {"values": "not an array"}}]';
|
||||
expect(() => tryToParseJsonToFormFields(json)).toThrowError(
|
||||
"Field dropdown in field 0 has no 'values' property that contains an array of options",
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject dropdown options that are not objects', () => {
|
||||
const json =
|
||||
'[{"fieldLabel": "Choice", "fieldType": "dropdown", "fieldOptions": {"values": [null]}}]';
|
||||
expect(() => tryToParseJsonToFormFields(json)).toThrowError(
|
||||
'Field dropdown in field 0 has an invalid option 0',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('binary', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user