PromucFlow_constructor/app/client/src/workers/validations.ts
Apeksha Bhosale 09eea59330
feat: JS Editor (#6003)
* Changes to add js plugin

* routes+reducer+create template

* added debugger to js editor page

* entity explorer changes

* create js function

* added copy, move and delete action

* added js plugin

* added existing js functions to data tree

* removed actionconfig for js collection

* new js function added to data tree and entity as well

* parsing flow added

* changes to data tree

* parse and update js functions

* small changes for def creator for js action

* create delete modified

* small changes for update

* update flow change

* entity properties added

* removed linting errors

* small changes in entity explorer

* changes for update

* move, copy implementation

* conflict resolved

* changes for dependecy map creation

* Only make the variables the binding paths

* Basic eval sync working

* Minor fixes

* removed unwanted code

* entity props and autocomplete

* saving in progress show

* redirection fix after delete js action

* removed unnecessary line

* Fixing merge conflict

* added sample body

* removed dummy data and added plugin Type

* few PR comments fixed

* automplete fix

* few more PR comments fix

* PR commnets fix

* move and copy api change

* js colleciton name refactor & 'move to page' changes & search

* view changes

* autocomplete added for js collections

* removing till async is implemented

* small changes

* separate js pane response view

* Executing functions

* js collection to js objects

* entity explorer issue and resolve action on page switch

* removed unused line

* small color fix

* js file icon added

* added js action to property pane

* Property pane changes for actions

* property pane changes for js functions

* showing syntax error for now

* actions sorted in response tab

* added js objects to slash and recent entitties

* enabling this to be used inside of function

* eval fix

* feature flag changes for entity explorer and property pane

* debugger changes

* copy bug fix

* small changes for eval

* debugger bug fix

* chnaged any to specific types

* error in console fix

* icons update

* fixed test case

* test case fix

* non empty check for functions

* evaluate test case fix

* added new icons

* text change

* updated time for debounce for trial

* after release mereg

* changed icon

* after merge

* PR comments simple

* fixed PR comments - redux form, settings remove

* js object interface changes

* name refactor

* export default change

* delete resolve actions chnage

* after merge

* adding execute fn as 3rd option and removed create new js function

* issue 7054 fixed - app crash

* execute function on response tab changes

* refactor function name part 1

* refactor of js function name

* try catch added refactor

* test fix

* not used line removed

* test cases locator fixed

Co-authored-by: Nidhi <nidhi.nair93@gmail.com>
Co-authored-by: hetunandu <hetu@appsmith.com>
2021-09-08 23:02:22 +05:30

715 lines
19 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ValidationTypes,
ValidationResponse,
Validator,
} from "../constants/WidgetValidation";
import _, {
get,
isArray,
isObject,
isPlainObject,
isString,
toString,
uniq,
} from "lodash";
import moment from "moment";
import { ValidationConfig } from "constants/PropertyControlConstants";
import evaluate from "./evaluate";
import getIsSafeURL from "utils/validation/getIsSafeURL";
export const UNDEFINED_VALIDATION = "UNDEFINED_VALIDATION";
function validatePlainObject(
config: ValidationConfig,
value: Record<string, unknown>,
props: Record<string, unknown>,
) {
if (config.params?.allowedKeys) {
let _valid = true;
const _messages: string[] = [];
config.params.allowedKeys.forEach((entry) => {
if (value.hasOwnProperty(entry.name)) {
const { isValid, message, parsed } = validate(
entry,
value[entry.name],
props,
);
if (!isValid) {
value[entry.name] = parsed;
_valid = isValid;
message &&
_messages.push(
`Value of key: ${entry.name} is invalid: ${message}`,
);
}
} else if (entry.params?.required) {
_valid = false;
_messages.push(`Missing required key: ${entry.name}`);
}
});
if (_valid) {
return {
isValid: true,
parsed: value,
};
}
return {
isValid: false,
parsed: config.params?.default || value,
message: _messages.join(" "),
};
}
return {
isValid: true,
parsed: value,
};
}
function validateArray(
config: ValidationConfig,
value: unknown[],
props: Record<string, unknown>,
) {
let _isValid = true;
const _messages: string[] = [];
const whiteList = config.params?.allowedValues;
if (whiteList) {
value.forEach((entry) => {
if (!whiteList.includes(entry)) {
return {
isValid: false,
parsed: value,
message: `Disallowed value: ${entry}`,
};
}
});
}
// Check uniqueness of object elements in an array
if (
config.params?.children?.type === ValidationTypes.OBJECT &&
(config.params.children.params?.allowedKeys || []).length > 0
) {
const allowedKeysCofigArray =
config.params.children.params?.allowedKeys || [];
const allowedKeys = (config.params.children.params?.allowedKeys || []).map(
(key) => key.name,
);
type ObjectKeys = typeof allowedKeys[number];
type ItemType = {
[key in ObjectKeys]: string;
};
const valueWithType = value as ItemType[];
allowedKeysCofigArray.forEach((allowedKeyConfig) => {
if (allowedKeyConfig.params?.unique) {
const allowedKeyValues = valueWithType.map(
(item) => item[allowedKeyConfig.name],
);
const normalizedValues = valueWithType.map((item) =>
item[allowedKeyConfig.name].toLowerCase(),
);
// Check if value is duplicated
_messages.push(
...allowedKeyValues.reduce(
(acc: string[], currentValue, currentIndex) => {
if (
normalizedValues.indexOf(currentValue.toLowerCase()) !==
currentIndex
) {
acc.push(
`Duplicated entry at index: ${currentIndex + 1}, key: ${
allowedKeyConfig.name
}, value: ${currentValue}`,
);
}
return acc;
},
[],
),
);
if (_messages.length > 0) {
_isValid = false;
}
}
});
}
const children = config.params?.children;
if (children) {
value.forEach((entry, index) => {
const validation = validate(children, entry, props);
if (!validation.isValid) {
_isValid = false;
_messages.push(
`Invalid entry at index: ${index}. ${validation.message}`,
);
}
});
}
if (config.params?.unique) {
if (isArray(config.params?.unique)) {
for (const param of config.params?.unique) {
const shouldBeUnique = value.map((entry) =>
get(entry, param as string, ""),
);
if (uniq(shouldBeUnique).length !== value.length) {
_isValid = false;
_messages.push(
`Array entry path:${param} must be unique. Duplicate values found`,
);
break;
}
}
} else if (
uniq(value.map((entry) => JSON.stringify(entry))).length !== value.length
) {
_isValid = false;
_messages.push(`Array must be unique. Duplicate values found`);
}
}
return {
isValid: _isValid,
parsed: _isValid ? value : config.params?.default || [],
message: _messages.join(" "),
};
}
export const validate = (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
) => {
const _result = VALIDATORS[config.type as ValidationTypes](
config,
value,
props,
);
return _result;
};
export const WIDGET_TYPE_VALIDATION_ERROR =
"This value does not evaluate to type"; // TODO: Lot's of changes in validations.ts file
export function getExpectedType(config?: ValidationConfig): string | undefined {
if (!config) return UNDEFINED_VALIDATION; // basic fallback
switch (config.type) {
case ValidationTypes.FUNCTION:
return config.params?.expected?.type || "unknown";
case ValidationTypes.TEXT:
let result = "string";
if (config.params?.allowedValues) {
const allowed = config.params.allowedValues.join(" | ");
result = result + ` ( ${allowed} )`;
}
if (config.params?.expected?.type) result = config.params?.expected.type;
return result;
case ValidationTypes.REGEX:
return "regExp";
case ValidationTypes.DATE_ISO_STRING:
return "ISO 8601 date string";
case ValidationTypes.BOOLEAN:
return "boolean";
case ValidationTypes.NUMBER:
let type = "number";
if (config.params?.min) {
type = `${type} Min: ${config.params?.min}`;
}
if (config.params?.max) {
type = `${type} Max: ${config.params?.max}`;
}
if (config.params?.required) {
type = `${type} Required`;
}
return type;
case ValidationTypes.OBJECT:
type = "Object";
if (config.params?.allowedKeys) {
type = "{";
config.params?.allowedKeys.forEach((allowedKeyConfig) => {
const _expected = getExpectedType(allowedKeyConfig);
type = `${type} "${allowedKeyConfig.name}": "${_expected}",`;
});
type = `${type.substring(0, type.length - 1)} }`;
return type;
}
return type;
case ValidationTypes.ARRAY:
if (config.params?.allowedValues) {
const allowed = config.params?.allowedValues.join("' | '");
return `Array<'${allowed}'>`;
}
if (config.params?.children) {
const children = getExpectedType(config.params.children);
return `Array<${children}>`;
}
return "Array";
case ValidationTypes.OBJECT_ARRAY:
return `Array<Object>`;
case ValidationTypes.IMAGE_URL:
return `base64 encoded image | data uri | image url`;
case ValidationTypes.SAFE_URL:
return "URL";
}
}
export const VALIDATORS: Record<ValidationTypes, Validator> = {
[ValidationTypes.TEXT]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
if (value === undefined || value === null) {
if (config.params && config.params.required) {
return {
isValid: false,
parsed: config.params?.default || "",
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
return {
isValid: true,
parsed: config.params?.default || "",
};
}
let parsed = value;
if (isObject(value)) {
return {
isValid: false,
parsed: JSON.stringify(value, null, 2),
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
const isValid = isString(parsed);
if (!isValid) {
try {
parsed = toString(parsed);
} catch (e) {
return {
isValid: false,
parsed: config.params?.default || "",
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
}
if (config.params?.allowedValues) {
if (!config.params?.allowedValues.includes((parsed as string).trim())) {
return {
parsed: config.params?.default || "",
message: "Value is not allowed",
isValid: false,
};
}
}
if (
config.params?.regex &&
isString(config.params?.regex) &&
!config.params?.regex.test(parsed as string)
) {
return {
parsed: config.params?.default || "",
message: `Value does not match expected regex: ${config.params?.regex.source}`,
isValid: false,
};
}
return {
isValid: true,
parsed,
};
},
// TODO(abhinav): The original validation does not make sense fix this.
[ValidationTypes.REGEX]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
const { isValid, message, parsed } = VALIDATORS[ValidationTypes.TEXT](
config,
value,
props,
);
if (!isValid) {
return {
isValid: false,
parsed: new RegExp(parsed),
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
return { isValid, parsed, message };
},
[ValidationTypes.NUMBER]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
if (value === undefined || value === null || value === "") {
if (config.params?.required) {
return {
isValid: false,
parsed: config.params?.default || 0,
message: "This value is required",
};
}
return {
isValid: true,
parsed: value,
};
}
if (!Number.isFinite(value) && !isString(value)) {
return {
isValid: false,
parsed: config.params?.default || 0,
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
// check for min and max limits
let parsed: number = value as number;
if (isString(value)) {
if (/^\d+\.?\d*$/.test(value)) {
parsed = Number(value);
} else {
return {
isValid: false,
parsed: config.params?.default || 0,
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
}
if (
config.params?.min !== undefined &&
Number.isFinite(config.params.min)
) {
if (parsed < Number(config.params.min)) {
return {
isValid: false,
parsed,
message: `Minimum allowed value: ${config.params.min}`,
};
}
}
if (
config.params?.max !== undefined &&
Number.isFinite(config.params.max)
) {
if (parsed > Number(config.params.max)) {
return {
isValid: false,
parsed,
message: `Maximum allowed value: ${config.params.max}`,
};
}
}
if (config.params?.natural && (parsed < 0 || !Number.isInteger(parsed))) {
return {
isValid: false,
parsed,
message: `Value should be a positive integer`,
};
}
return {
isValid: true,
parsed,
};
},
[ValidationTypes.BOOLEAN]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
if (value === undefined || value === null || value === "") {
if (config.params && config.params.required) {
return {
isValid: false,
parsed: !!config.params?.default,
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
return { isValid: true, parsed: config.params?.default || value };
}
const isABoolean = value === true || value === false;
const isStringTrueFalse = value === "true" || value === "false";
const isValid = isABoolean || isStringTrueFalse;
let parsed = value;
if (isStringTrueFalse) parsed = value !== "false";
if (!isValid) {
return {
isValid: false,
parsed: config.params?.default || false,
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
}
return { isValid, parsed };
},
[ValidationTypes.OBJECT]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
if (
value === undefined ||
value === null ||
(isString(value) && value.trim().length === 0)
) {
if (config.params && config.params.required) {
return {
isValid: false,
parsed: config.params?.default || {},
message: `${WIDGET_TYPE_VALIDATION_ERROR}: ${getExpectedType(
config,
)}`,
};
}
return {
isValid: true,
parsed: config.params?.default || value,
};
}
if (isPlainObject(value)) {
return validatePlainObject(
config,
value as Record<string, unknown>,
props,
);
}
try {
const result = { parsed: JSON.parse(value as string), isValid: true };
if (isPlainObject(result.parsed)) {
return validatePlainObject(config, result.parsed, props);
}
return {
isValid: false,
parsed: config.params?.default || {},
message: `${WIDGET_TYPE_VALIDATION_ERROR}: ${getExpectedType(config)}`,
};
} catch (e) {
return {
isValid: false,
parsed: config.params?.default || {},
message: `${WIDGET_TYPE_VALIDATION_ERROR}: ${getExpectedType(config)}`,
};
}
},
[ValidationTypes.ARRAY]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
const invalidResponse = {
isValid: false,
parsed: config.params?.default || [],
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
if (value === undefined || value === null || value === "") {
if (config.params && config.params.required) {
invalidResponse.message =
"This property is required for the widget to function correctly";
return invalidResponse;
}
return {
isValid: true,
parsed: value,
};
}
if (isString(value)) {
try {
const _value = JSON.parse(value);
if (Array.isArray(_value)) {
const result = validateArray(config, _value, props);
return result;
}
} catch (e) {
return invalidResponse;
}
}
if (Array.isArray(value)) {
return validateArray(config, value, props);
}
return invalidResponse;
},
[ValidationTypes.OBJECT_ARRAY]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
const invalidResponse = {
isValid: false,
parsed: config.params?.default || [{}],
message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`,
};
if (value === undefined || value === null || value === "") {
if (config.params?.required) return invalidResponse;
return { isValid: true, parsed: value };
}
if (!isString(value) && !Array.isArray(value)) {
return invalidResponse;
}
let parsed = value;
if (isString(value)) {
try {
parsed = JSON.parse(value);
} catch (e) {
return invalidResponse;
}
}
if (Array.isArray(parsed)) {
if (parsed.length === 0) return invalidResponse;
for (const [index, parsedEntry] of parsed.entries()) {
if (!isPlainObject(parsedEntry)) {
return {
...invalidResponse,
message: `Invalid object at index ${index}`,
};
}
}
return { isValid: true, parsed };
}
return invalidResponse;
},
[ValidationTypes.DATE_ISO_STRING]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
const invalidResponse = {
isValid: false,
parsed: config.params?.default || moment().toISOString(true),
message: `Value does not match: ${getExpectedType(config)}`,
};
if (value === undefined || value === null || !isString(value)) {
if (!config.params?.required) {
return {
isValid: true,
parsed: value,
};
}
return invalidResponse;
}
if (isString(value)) {
if (!moment(value).isValid()) return invalidResponse;
if (
value === moment(value).toISOString() ||
value === moment(value).toISOString(true)
) {
return {
isValid: true,
parsed: value,
};
}
if (moment(value).isValid())
return { isValid: true, parsed: moment(value).toISOString(true) };
}
return invalidResponse;
},
[ValidationTypes.FUNCTION]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
const invalidResponse = {
isValid: false,
parsed: undefined,
message: "Failed to validate",
};
if (config.params?.fnString && isString(config.params?.fnString)) {
try {
const { result } = evaluate(config.params.fnString, {}, {}, [
value,
props,
_,
moment,
]);
return result;
} catch (e) {
console.error("Validation function error: ", { e });
}
}
return invalidResponse;
},
[ValidationTypes.IMAGE_URL]: (
config: ValidationConfig,
value: unknown,
props: Record<string, unknown>,
): ValidationResponse => {
const invalidResponse = {
isValid: false,
parsed: config.params?.default || "",
message: `${WIDGET_TYPE_VALIDATION_ERROR}: ${getExpectedType(config)}`,
};
const base64Regex = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
const base64ImageRegex = /^data:image\/.*;base64/;
const imageUrlRegex = /(http(s?):)([/|.|\w|\s|-])*\.(?:jpeg|jpg|gif|png)??(?:&?[^=&]*=[^=&]*)*/;
if (
value === undefined ||
value === null ||
(isString(value) && value.trim().length === 0)
) {
if (config.params && config.params.required) return invalidResponse;
return { isValid: true, parsed: value };
}
if (isString(value)) {
if (imageUrlRegex.test(value.trim())) {
return { isValid: true, parsed: value.trim() };
}
if (base64ImageRegex.test(value)) {
return {
isValid: true,
parsed: value,
};
}
if (base64Regex.test(value) && btoa(atob(value)) === value) {
return { isValid: true, parsed: `data:image/png;base64,${value}` };
}
}
return invalidResponse;
},
[ValidationTypes.SAFE_URL]: (
config: ValidationConfig,
value: unknown,
): ValidationResponse => {
const invalidResponse = {
isValid: false,
parsed: config?.params?.default || "",
message: `${WIDGET_TYPE_VALIDATION_ERROR}: ${getExpectedType(config)}`,
};
if (typeof value === "string" && getIsSafeURL(value)) {
return {
isValid: true,
parsed: value,
};
} else {
return invalidResponse;
}
},
};