mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Switch from ajv to io-ts for config validation; validate configs on save in the API/dashboard; start work on creating io-ts schemas for all plugins
This commit is contained in:
parent
b230a73a6f
commit
da114c0e60
14 changed files with 256 additions and 41 deletions
|
@ -3,6 +3,19 @@ const apiUrl = process.env.API_URL;
|
|||
|
||||
type QueryParamObject = { [key: string]: string | null };
|
||||
|
||||
export class ApiError extends Error {
|
||||
protected body: object;
|
||||
protected status: number;
|
||||
protected res: Response;
|
||||
|
||||
constructor(message: string, body: object, status: number, res: Response) {
|
||||
super(message);
|
||||
this.body = body;
|
||||
this.status = status;
|
||||
this.res = res;
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryString(params: QueryParamObject) {
|
||||
if (Object.keys(params).length === 0) return "";
|
||||
|
||||
|
@ -15,7 +28,14 @@ function buildQueryString(params: QueryParamObject) {
|
|||
}
|
||||
|
||||
export function request(resource, fetchOpts: RequestInit = {}) {
|
||||
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(res => res.json());
|
||||
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(async res => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json();
|
||||
throw new ApiError(res.statusText, body, res.status, res);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
export function get(resource: string, params: QueryParamObject = {}) {
|
||||
|
@ -32,7 +52,7 @@ export function post(resource: string, params: QueryParamObject = {}) {
|
|||
const headers: Record<string, string> = RootStore.state.auth.apiKey
|
||||
? { "X-Api-Key": RootStore.state.auth.apiKey }
|
||||
: {};
|
||||
return request(resource + buildQueryString(params), {
|
||||
return request(resource, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
|
|
|
@ -5,8 +5,17 @@
|
|||
<div v-else>
|
||||
<h2 class="title is-1">Config for {{ guild.name }}</h2>
|
||||
<codemirror v-model="editableConfig" :options="cmConfig"></codemirror>
|
||||
<button class="button" v-on:click="save" :disabled="saving">Save</button>
|
||||
<span v-if="saving">Saving...</span>
|
||||
<div class="editor-footer">
|
||||
<div class="actions">
|
||||
<button class="button" v-on:click="save" :disabled="saving">Save</button>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div class="status-message" v-if="saving">Saving...</div>
|
||||
<div class="status-message error" v-if="errors.length">
|
||||
<div v-for="error in errors">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -26,6 +35,7 @@
|
|||
import "codemirror/lib/codemirror.css";
|
||||
import "codemirror/theme/oceanic-next.css";
|
||||
import "codemirror/mode/yaml/yaml.js";
|
||||
import {ApiError} from "../api";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -47,6 +57,7 @@
|
|||
loading: true,
|
||||
saving: false,
|
||||
editableConfig: null,
|
||||
errors: [],
|
||||
cmConfig: {
|
||||
indentWithTabs: false,
|
||||
indentUnit: 2,
|
||||
|
@ -69,10 +80,23 @@
|
|||
methods: {
|
||||
async save() {
|
||||
this.saving = true;
|
||||
await this.$store.dispatch("guilds/saveConfig", {
|
||||
guildId: this.$route.params.guildId,
|
||||
config: this.editableConfig,
|
||||
});
|
||||
this.errors = [];
|
||||
|
||||
try {
|
||||
await this.$store.dispatch("guilds/saveConfig", {
|
||||
guildId: this.$route.params.guildId,
|
||||
config: this.editableConfig,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && (e.status === 400 || e.status === 422)) {
|
||||
this.errors = e.body.errors || ['Error while saving config'];
|
||||
this.saving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.$router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
|
|
|
@ -19,7 +19,6 @@ Vue.mixin({
|
|||
|
||||
import App from "./components/App.vue";
|
||||
import Login from "./components/Login.vue";
|
||||
import { get } from "./api";
|
||||
|
||||
const app = new Vue({
|
||||
router,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { get, hasApiKey, post, setApiKey } from "../api";
|
||||
import { get, post } from "../api";
|
||||
import { ActionTree, Module } from "vuex";
|
||||
import { AuthState, RootState } from "./types";
|
||||
|
||||
|
@ -16,13 +16,16 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
|
||||
const storedKey = localStorage.getItem("apiKey");
|
||||
if (storedKey) {
|
||||
const result = await post("auth/validate-key", { key: storedKey });
|
||||
if (result.valid) {
|
||||
await dispatch("setApiKey", storedKey);
|
||||
} else {
|
||||
console.log("Unable to validate key, removing from localStorage");
|
||||
localStorage.removeItem("apiKey");
|
||||
}
|
||||
try {
|
||||
const result = await post("auth/validate-key", { key: storedKey });
|
||||
if (result.valid) {
|
||||
await dispatch("setApiKey", storedKey);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
console.log("Unable to validate key, removing from localStorage");
|
||||
localStorage.removeItem("apiKey");
|
||||
}
|
||||
|
||||
commit("markInitialAuthLoaded");
|
||||
|
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -1904,6 +1904,12 @@
|
|||
"integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/js-yaml": {
|
||||
"version": "3.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz",
|
||||
"integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.110",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.110.tgz",
|
||||
|
@ -2057,6 +2063,7 @@
|
|||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz",
|
||||
"integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
@ -3860,12 +3867,14 @@
|
|||
"fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||
"dev": true
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
|
||||
"dev": true
|
||||
},
|
||||
"fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
|
@ -4002,6 +4011,11 @@
|
|||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||
},
|
||||
"fp-ts": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.0.1.tgz",
|
||||
"integrity": "sha512-tw/L1f6hd29bYk3IlhA3qdnqz9LJ13yeecjk0XqC8g9E76P5ZXzNcjXA5zQ0zNlsGhdVhDIHRJB738oGpB8PnA=="
|
||||
},
|
||||
"fragment-cache": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
|
||||
|
@ -5057,6 +5071,11 @@
|
|||
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
|
||||
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
|
||||
},
|
||||
"io-ts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.0.0.tgz",
|
||||
"integrity": "sha512-6i8PKyNR/dvEbUU9uE+v4iVFU7l674ZEGQsh92y6xEZF/rj46fXbPy+uPPXJEsCP0J0X3UpzXAxp04K4HR2jVw=="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
|
||||
|
@ -7771,7 +7790,8 @@
|
|||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
|
@ -9196,7 +9216,8 @@
|
|||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
|
@ -10780,6 +10801,7 @@
|
|||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"test": "jest src"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^6.7.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^5.2.0",
|
||||
"dotenv": "^4.0.0",
|
||||
|
@ -26,7 +25,9 @@
|
|||
"eris": "^0.10.0",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"express": "^4.17.0",
|
||||
"fp-ts": "^2.0.1",
|
||||
"humanize-duration": "^3.15.0",
|
||||
"io-ts": "^2.0.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"knub": "^20.1.0",
|
||||
"last-commit-log": "^2.1.0",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"@types/cors": "^2.8.5",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/jest": "^24.0.11",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/lodash.at": "^4.6.3",
|
||||
"@types/moment-timezone": "^0.5.6",
|
||||
"@types/node": "^10.12.0",
|
||||
|
|
|
@ -3,9 +3,11 @@ import passport from "passport";
|
|||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||
import { requireAPIToken } from "./auth";
|
||||
import { ApiPermissions } from "../data/ApiPermissions";
|
||||
import { clientError, ok, unauthorized } from "./responses";
|
||||
import { clientError, error, ok, serverError, unauthorized } from "./responses";
|
||||
import { Configs } from "../data/Configs";
|
||||
import { ApiRoles } from "../data/ApiRoles";
|
||||
import { validateGuildConfig } from "../configValidator";
|
||||
import yaml, { YAMLException } from "js-yaml";
|
||||
|
||||
export function initGuildsAPI(app: express.Express) {
|
||||
const guildAPIRouter = express.Router();
|
||||
|
@ -35,6 +37,28 @@ export function initGuildsAPI(app: express.Express) {
|
|||
const config = req.body.config;
|
||||
if (config == null) return clientError(res, "No config supplied");
|
||||
|
||||
// Validate config
|
||||
let parsedConfig;
|
||||
try {
|
||||
parsedConfig = yaml.safeLoad(config);
|
||||
} catch (e) {
|
||||
if (e instanceof YAMLException) {
|
||||
return error(res, e.message, 400);
|
||||
}
|
||||
|
||||
console.error("Error when loading YAML: " + e.message);
|
||||
return serverError(res, "Server error");
|
||||
}
|
||||
|
||||
if (parsedConfig == null) {
|
||||
parsedConfig = {};
|
||||
}
|
||||
|
||||
const errors = validateGuildConfig(parsedConfig);
|
||||
if (errors) {
|
||||
return res.status(422).json({ errors });
|
||||
}
|
||||
|
||||
await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId);
|
||||
ok(res);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ export function error(res: Response, message: string, statusCode: number = 500)
|
|||
res.status(statusCode).json({ error: message });
|
||||
}
|
||||
|
||||
export function serverError(res: Response, message: string) {
|
||||
export function serverError(res: Response, message = "Server error") {
|
||||
error(res, message, 500);
|
||||
}
|
||||
|
||||
|
|
51
src/configValidator.ts
Normal file
51
src/configValidator.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import * as t from "io-ts";
|
||||
import { IPluginOptions } from "knub";
|
||||
import { pipe } from "fp-ts/lib/pipeable";
|
||||
import { fold } from "fp-ts/lib/Either";
|
||||
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||
import { availablePlugins } from "./plugins/availablePlugins";
|
||||
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
||||
import { validateStrict } from "./validatorUtils";
|
||||
|
||||
const pluginNameToClass = new Map<string, typeof ZeppelinPlugin>();
|
||||
for (const pluginClass of availablePlugins) {
|
||||
// @ts-ignore
|
||||
pluginNameToClass.set(pluginClass.pluginName, pluginClass);
|
||||
}
|
||||
|
||||
const guildConfigRootSchema = t.type({
|
||||
prefix: t.string,
|
||||
levels: t.record(t.string, t.number),
|
||||
plugins: t.record(t.string, t.unknown),
|
||||
});
|
||||
const partialGuildConfigRootSchema = t.exact(t.partial(guildConfigRootSchema.props));
|
||||
|
||||
const globalConfigRootSchema = t.type({
|
||||
url: t.string,
|
||||
owners: t.array(t.string),
|
||||
plugins: t.record(t.string, t.unknown),
|
||||
});
|
||||
|
||||
const partialMegaTest = t.partial({ name: t.string });
|
||||
|
||||
export function validateGuildConfig(config: any): string[] | null {
|
||||
const rootErrors = validateStrict(partialGuildConfigRootSchema, config);
|
||||
if (rootErrors) return rootErrors;
|
||||
|
||||
if (config.plugins) {
|
||||
for (const [pluginName, pluginOptions] of Object.entries(config.plugins)) {
|
||||
if (!pluginNameToClass.has(pluginName)) {
|
||||
return [`Unknown plugin: ${pluginName}`];
|
||||
}
|
||||
|
||||
const pluginClass = pluginNameToClass.get(pluginName);
|
||||
let pluginErrors = pluginClass.validateOptions(pluginOptions);
|
||||
if (pluginErrors) {
|
||||
pluginErrors = pluginErrors.map(err => `${pluginName}: ${err}`);
|
||||
return pluginErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -5,20 +5,23 @@ import { GuildAutoReactions } from "../data/GuildAutoReactions";
|
|||
import { Message } from "eris";
|
||||
import { customEmojiRegex, errorMessage, isEmoji, successMessage } from "../utils";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
import * as t from "io-ts";
|
||||
|
||||
interface IAutoReactionsPluginConfig {
|
||||
can_manage: boolean;
|
||||
}
|
||||
const configSchema = t.type({
|
||||
can_manage: t.boolean,
|
||||
});
|
||||
type TConfigSchema = t.TypeOf<typeof configSchema>;
|
||||
|
||||
export class AutoReactionsPlugin extends ZeppelinPlugin<IAutoReactionsPluginConfig> {
|
||||
export class AutoReactionsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
||||
public static pluginName = "auto_reactions";
|
||||
protected static configSchema = configSchema;
|
||||
|
||||
protected savedMessages: GuildSavedMessages;
|
||||
protected autoReactions: GuildAutoReactions;
|
||||
|
||||
private onMessageCreateFn;
|
||||
|
||||
getDefaultOptions(): IPluginOptions<IAutoReactionsPluginConfig> {
|
||||
getDefaultOptions(): IPluginOptions<TConfigSchema> {
|
||||
return {
|
||||
config: {
|
||||
can_manage: false,
|
||||
|
|
39
src/plugins/GlobalZeppelinPlugin.ts
Normal file
39
src/plugins/GlobalZeppelinPlugin.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { GlobalPlugin, IBasePluginConfig, IPluginOptions, logger } from "knub";
|
||||
import { PluginRuntimeError } from "../PluginRuntimeError";
|
||||
import * as t from "io-ts";
|
||||
import { pipe } from "fp-ts/lib/pipeable";
|
||||
import { fold } from "fp-ts/lib/Either";
|
||||
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||
import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, UnknownUser } from "../utils";
|
||||
import { Member, User } from "eris";
|
||||
import { performance } from "perf_hooks";
|
||||
import { validateStrict } from "../validatorUtils";
|
||||
|
||||
const SLOW_RESOLVE_THRESHOLD = 1500;
|
||||
|
||||
export class GlobalZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends GlobalPlugin<TConfig> {
|
||||
protected static configSchema: t.TypeC<any>;
|
||||
public static dependencies = [];
|
||||
|
||||
public static validateOptions(options: IPluginOptions): string[] | null {
|
||||
// Validate config values
|
||||
if (this.configSchema) {
|
||||
if (options.config) {
|
||||
const errors = validateStrict(this.configSchema, options.config);
|
||||
if (errors) return errors;
|
||||
}
|
||||
|
||||
if (options.overrides) {
|
||||
for (const override of options.overrides) {
|
||||
if (override.config) {
|
||||
const errors = validateStrict(this.configSchema, override.config);
|
||||
if (errors) return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No errors, return null
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -2,12 +2,13 @@ import { Plugin, decorators as d, IPluginOptions } from "knub";
|
|||
import { GuildChannel, Message, TextChannel } from "eris";
|
||||
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
||||
import { successMessage } from "../utils";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
|
||||
interface IMessageSaverPluginConfig {
|
||||
can_manage: boolean;
|
||||
}
|
||||
|
||||
export class MessageSaverPlugin extends Plugin<IMessageSaverPluginConfig> {
|
||||
export class MessageSaverPlugin extends ZeppelinPlugin<IMessageSaverPluginConfig> {
|
||||
public static pluginName = "message_saver";
|
||||
|
||||
protected savedMessages: GuildSavedMessages;
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { IBasePluginConfig, IPluginOptions, logger, Plugin } from "knub";
|
||||
import { PluginRuntimeError } from "../PluginRuntimeError";
|
||||
import Ajv, { ErrorObject } from "ajv";
|
||||
import * as t from "io-ts";
|
||||
import { pipe } from "fp-ts/lib/pipeable";
|
||||
import { fold } from "fp-ts/lib/Either";
|
||||
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||
import { isSnowflake, isUnicodeEmoji, resolveMember, resolveUser, UnknownUser } from "../utils";
|
||||
import { Member, User } from "eris";
|
||||
|
||||
import { performance } from "perf_hooks";
|
||||
import { validateStrict } from "../validatorUtils";
|
||||
|
||||
const SLOW_RESOLVE_THRESHOLD = 1500;
|
||||
|
||||
export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plugin<TConfig> {
|
||||
protected static configSchema: any;
|
||||
protected static configSchema: t.TypeC<any>;
|
||||
public static dependencies = [];
|
||||
|
||||
protected throwPluginRuntimeError(message: string) {
|
||||
|
@ -26,22 +29,19 @@ export class ZeppelinPlugin<TConfig extends {} = IBasePluginConfig> extends Plug
|
|||
return ourLevel > memberLevel;
|
||||
}
|
||||
|
||||
public static validateOptions(options: IPluginOptions): ErrorObject[] | null {
|
||||
public static validateOptions(options: any): string[] | null {
|
||||
// Validate config values
|
||||
if (this.configSchema) {
|
||||
const ajv = new Ajv();
|
||||
const validate = ajv.compile(this.configSchema);
|
||||
|
||||
if (options.config) {
|
||||
const isValid = validate(options.config);
|
||||
if (!isValid) return validate.errors;
|
||||
const errors = validateStrict(this.configSchema, options.config);
|
||||
if (errors) return errors;
|
||||
}
|
||||
|
||||
if (options.overrides) {
|
||||
for (const override of options.overrides) {
|
||||
if (override.config) {
|
||||
const isValid = validate(override.config);
|
||||
if (!isValid) return validate.errors;
|
||||
const errors = validateStrict(this.configSchema, override.config);
|
||||
if (errors) return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
src/validatorUtils.ts
Normal file
27
src/validatorUtils.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as t from "io-ts";
|
||||
import { pipe } from "fp-ts/lib/pipeable";
|
||||
import { fold } from "fp-ts/lib/Either";
|
||||
import { PathReporter } from "io-ts/lib/PathReporter";
|
||||
|
||||
/**
|
||||
* Validates the given value against the given schema while also disallowing extra properties
|
||||
* See: https://github.com/gcanti/io-ts/issues/322
|
||||
*/
|
||||
export function validateStrict(schema: t.Type<any, any, any>, value: any): string[] | null {
|
||||
const validationResult = schema.decode(value);
|
||||
return pipe(
|
||||
validationResult,
|
||||
fold(
|
||||
err => PathReporter.report(validationResult),
|
||||
result => {
|
||||
// Make sure there are no extra properties
|
||||
if (JSON.stringify(value) !== JSON.stringify(result)) {
|
||||
// TODO: Actually mention what the unknown property is
|
||||
return ["Found unknown properties"];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
Loading…
Add table
Reference in a new issue