From 0386fad962ed5572e6426ddd595107c378143df0 Mon Sep 17 00:00:00 2001 From: juzhiyuan Date: Mon, 14 Dec 2020 16:18:21 +0800 Subject: [PATCH 1/5] feat: use ajv to validate data --- web/package.json | 2 +- web/src/components/Plugin/PluginPage.tsx | 71 +++++++++++++++++------- web/src/components/Plugin/service.ts | 4 +- web/yarn.lock | 22 +++++++- 4 files changed, 76 insertions(+), 23 deletions(-) diff --git a/web/package.json b/web/package.json index e79ad5724d..fd590b226c 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..a5c15995e8 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, { JSONSchemaType, 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,31 +58,62 @@ 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; + } else { + console.log(validate.errors) + 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, + }); + } + setName(pluginName); } - errors?.forEach((item) => { - notification.error({ - message: 'Invalid plugin data', - description: item.message, - }); - }); - setName(pluginName); }); }; 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 b919dd8d36..8529b4bc71 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -3862,6 +3862,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" @@ -10388,12 +10398,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= @@ -15135,6 +15150,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" From 47a8ba75db4235b985184d9c495e19dcd70eb4f3 Mon Sep 17 00:00:00 2001 From: juzhiyuan Date: Mon, 14 Dec 2020 16:23:30 +0800 Subject: [PATCH 2/5] style: format codes --- web/src/components/Plugin/PluginPage.tsx | 58 ++++++++++++------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index a5c15995e8..8b601cd9ce 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 Ajv, { JSONSchemaType, DefinedError } from "ajv" +import Ajv, { DefinedError } from 'ajv'; import { fetchSchema, getList } from './service'; import CodeMirrorDrawer from './CodeMirrorDrawer'; @@ -43,13 +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 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); @@ -63,49 +63,51 @@ const PluginPage: React.FC = ({ // 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 = {} + schema.properties = {}; } // eslint-disable-next-line - ; (schema.properties as any).disable = { - type: "boolean" - } - return schema - } + (schema.properties as any).disable = { + type: 'boolean', + }; + return schema; + }; const validateData = (pluginName: string, value: PluginComponent.Data) => { fetchSchema(pluginName, schemaType).then((schema) => { if (schema.oneOf) { (schema.oneOf || []).forEach((item: any) => { - injectDisableProperty(item) - }) + injectDisableProperty(item); + }); } else { - injectDisableProperty(schema) + injectDisableProperty(schema); } - const validate = ajv.compile(schema) + const validate = ajv.compile(schema); if (validate(value)) { setName(NEVER_EXIST_PLUGIN_FLAG); onChange({ ...initialData, [pluginName]: value }); return; } else { - console.log(validate.errors) + console.log(validate.errors); for (const err of validate.errors as DefinedError[]) { - let description = "" + 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 + 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}` + description = `${err.schemaPath} ${err.message}`; } notification.error({ message: 'Invalid plugin data', @@ -191,7 +193,7 @@ const PluginPage: React.FC = ({ if (isChecked) { validateData(item.name, { ...initialData[item.name], - disable: false + disable: false, }); } else { onChange({ From 74445ed4c5c7fdc28066dcab2b2c7f77a70d2bf0 Mon Sep 17 00:00:00 2001 From: juzhiyuan Date: Mon, 14 Dec 2020 16:26:11 +0800 Subject: [PATCH 3/5] style: format codes --- web/src/components/Plugin/PluginPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index 8b601cd9ce..c9d95b3b39 100644 --- a/web/src/components/Plugin/PluginPage.tsx +++ b/web/src/components/Plugin/PluginPage.tsx @@ -75,7 +75,7 @@ const PluginPage: React.FC = ({ const validateData = (pluginName: string, value: PluginComponent.Data) => { fetchSchema(pluginName, schemaType).then((schema) => { if (schema.oneOf) { - (schema.oneOf || []).forEach((item: any) => { + ;(schema.oneOf || []).forEach((item: any) => { injectDisableProperty(item); }); } else { @@ -89,7 +89,6 @@ const PluginPage: React.FC = ({ onChange({ ...initialData, [pluginName]: value }); return; } else { - console.log(validate.errors); for (const err of validate.errors as DefinedError[]) { let description = ''; switch (err.keyword) { From 569dc127af8bb50abb7895a75bfccdf71bbd2cf3 Mon Sep 17 00:00:00 2001 From: juzhiyuan Date: Mon, 14 Dec 2020 17:08:13 +0800 Subject: [PATCH 4/5] style: remove extra ; --- web/src/components/Plugin/PluginPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index c9d95b3b39..748f065954 100644 --- a/web/src/components/Plugin/PluginPage.tsx +++ b/web/src/components/Plugin/PluginPage.tsx @@ -75,7 +75,7 @@ const PluginPage: React.FC = ({ const validateData = (pluginName: string, value: PluginComponent.Data) => { fetchSchema(pluginName, schemaType).then((schema) => { if (schema.oneOf) { - ;(schema.oneOf || []).forEach((item: any) => { + (schema.oneOf || []).forEach((item: any) => { injectDisableProperty(item); }); } else { From 8b88472fabf1b975bb2174281b0171f06258a95c Mon Sep 17 00:00:00 2001 From: juzhiyuan Date: Mon, 14 Dec 2020 17:28:07 +0800 Subject: [PATCH 5/5] style: format codes --- web/src/components/Plugin/PluginPage.tsx | 53 ++++++++++++------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/web/src/components/Plugin/PluginPage.tsx b/web/src/components/Plugin/PluginPage.tsx index 748f065954..e9b10af0d4 100644 --- a/web/src/components/Plugin/PluginPage.tsx +++ b/web/src/components/Plugin/PluginPage.tsx @@ -49,7 +49,7 @@ const PluginPage: React.FC = ({ readonly = false, initialData = {}, schemaType = '', - onChange = () => {}, + onChange = () => { }, }) => { const [pluginList, setPlugin] = useState([]); const [name, setName] = useState(NEVER_EXIST_PLUGIN_FLAG); @@ -88,33 +88,34 @@ const PluginPage: React.FC = ({ setName(NEVER_EXIST_PLUGIN_FLAG); onChange({ ...initialData, [pluginName]: value }); return; - } else { - 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, - }); + } + + // 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}`; } - setName(pluginName); + notification.error({ + message: 'Invalid plugin data', + description, + }); } + setName(pluginName); }); };