From 8b07c437f93f3be1fbf0320d021ce9115a7c4ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=90=9A=E8=87=B4=E8=BF=9C?= Date: Fri, 18 Dec 2020 22:05:53 +0800 Subject: [PATCH] feat(plugin): use ajv to validate data (#1047) * feat: use ajv to validate data * style: format codes * style: format codes * style: remove extra ; * style: format codes --- web/package.json | 2 +- web/src/components/Plugin/PluginPage.tsx | 67 ++++++++++++++++++------ web/src/components/Plugin/service.ts | 4 +- web/yarn.lock | 22 +++++++- 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/web/package.json b/web/package.json index e66e9de849..8a6382ad8f 100644 --- a/web/package.json +++ b/web/package.json @@ -54,11 +54,11 @@ "@rjsf/antd": "2.2.0", "@rjsf/core": "2.2.0", "@uiw/react-codemirror": "^3.0.1", + "ajv": "^7.0.0-rc.2", "antd": "^4.4.0", "classnames": "^2.2.6", "dayjs": "1.8.28", "js-beautify": "^1.13.0", - "json-schema": "0.2.5", "lodash": "^4.17.11", "moment": "^2.25.3", "nzh": "1.0.4", diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index 4224af226b..e9b10af0d4 100644 --- a/web/src/components/Plugin/PluginPage.tsx +++ b/web/src/components/Plugin/PluginPage.tsx @@ -18,7 +18,7 @@ import React, { useEffect, useState } from 'react'; import { Anchor, Layout, Switch, Card, Tooltip, Button, notification, Avatar } from 'antd'; import { SettingFilled } from '@ant-design/icons'; import { PanelSection } from '@api7-dashboard/ui'; -import { validate } from 'json-schema'; +import Ajv, { DefinedError } from 'ajv'; import { fetchSchema, getList } from './service'; import CodeMirrorDrawer from './CodeMirrorDrawer'; @@ -43,11 +43,13 @@ const { Sider, Content } = Layout; // NOTE: use this flag as plugin's name to hide drawer const NEVER_EXIST_PLUGIN_FLAG = 'NEVER_EXIST_PLUGIN_FLAG'; +const ajv = new Ajv(); + const PluginPage: React.FC = ({ readonly = false, initialData = {}, schemaType = '', - onChange = () => {}, + onChange = () => { }, }) => { const [pluginList, setPlugin] = useState([]); const [name, setName] = useState(NEVER_EXIST_PLUGIN_FLAG); @@ -56,30 +58,63 @@ const PluginPage: React.FC = ({ getList().then(setPlugin); }, []); + // NOTE: This function has side effect because it mutates the original schema data + const injectDisableProperty = (schema: Record) => { + // NOTE: The frontend will inject the disable property into schema just like the manager-api does + if (!schema.properties) { + // eslint-disable-next-line + schema.properties = {}; + } + // eslint-disable-next-line + (schema.properties as any).disable = { + type: 'boolean', + }; + return schema; + }; + const validateData = (pluginName: string, value: PluginComponent.Data) => { fetchSchema(pluginName, schemaType).then((schema) => { - // NOTE: The frontend will inject the disable property into schema just like the manager-api does - if (!schema.properties) { - // eslint-disable-next-line - schema.properties = {} - } - // eslint-disable-next-line - ;(schema.properties as any).disable = { - type: "boolean" + if (schema.oneOf) { + (schema.oneOf || []).forEach((item: any) => { + injectDisableProperty(item); + }); + } else { + injectDisableProperty(schema); } - const { valid, errors } = validate(value, schema); - if (valid) { + const validate = ajv.compile(schema); + + if (validate(value)) { setName(NEVER_EXIST_PLUGIN_FLAG); onChange({ ...initialData, [pluginName]: value }); return; } - errors?.forEach((item) => { + + // eslint-disable-next-line + for (const err of validate.errors as DefinedError[]) { + let description = ''; + switch (err.keyword) { + case 'enum': + description = `${err.dataPath} ${err.message}: ${err.params.allowedValues.join( + ', ', + )}`; + break; + case 'minItems': + case 'type': + description = `${err.dataPath} ${err.message}`; + break; + case 'oneOf': + case 'required': + description = err.message || ''; + break; + default: + description = `${err.schemaPath} ${err.message}`; + } notification.error({ message: 'Invalid plugin data', - description: item.message, + description, }); - }); + } setName(pluginName); }); }; @@ -158,7 +193,7 @@ const PluginPage: React.FC = ({ if (isChecked) { validateData(item.name, { ...initialData[item.name], - disable: false + disable: false, }); } else { onChange({ diff --git a/web/src/components/Plugin/service.ts b/web/src/components/Plugin/service.ts index 8bbcc2ce61..1f94ab6d37 100644 --- a/web/src/components/Plugin/service.ts +++ b/web/src/components/Plugin/service.ts @@ -14,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema7 } from 'json-schema'; import { omit } from 'lodash'; import { request } from 'umi'; + import { PLUGIN_MAPPER_SOURCE } from './data'; enum Category { @@ -80,7 +80,7 @@ const cachedPluginSchema: Record = { export const fetchSchema = async ( name: string, schemaType: PluginComponent.Schema, -): Promise => { +): Promise => { if (!cachedPluginSchema[schemaType][name]) { const queryString = schemaType !== 'route' ? `?schema_type=${schemaType}` : ''; cachedPluginSchema[schemaType][name] = ( diff --git a/web/yarn.lock b/web/yarn.lock index f9c779637d..eacd0924f8 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -3923,6 +3923,16 @@ ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.0.0-rc.2: + version "7.0.0-rc.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.0-rc.2.tgz#9c237b95072c1ee8c38e2df76422f37bacc9ae5e" + integrity sha512-D2iqHvbT3lszv5KSsTvJL9PSPf/2/s45i68vLXJmT124cxK/JOoOFyo/QnrgMKa2FHlVaMIsp1ZN1P4EH3bCKw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.npm.taobao.org/alphanum-sort/download/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -10654,12 +10664,17 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha1-afaofZUTq4u4/mO9sJecRI5oRmA= +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.npm.taobao.org/json-schema/download/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= -json-schema@0.2.5, json-schema@^0.2.5: +json-schema@^0.2.5: version "0.2.5" resolved "https://registry.npm.taobao.org/json-schema/download/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" integrity sha1-l5l/UJct0FACFOIIxAfvpLXXBjs= @@ -15485,6 +15500,11 @@ require-directory@^2.1.1: resolved "https://registry.npm.taobao.org/require-directory/download/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.npm.taobao.org/require-main-filename/download/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"