UNPKG

3.36 kBJavaScriptView Raw
1// @ts-check
2
3import Ajv from "ajv";
4import { get, merge, cloneDeep } from "lodash";
5import Debug from "debug";
6
7const debug = Debug("store:validate");
8
9/**
10 * override oneOf keywords support removeAdditional option
11 * @param {import('ajv').Ajv} ajv
12 */
13function oneOfRA(ajv) {
14 ajv.removeKeyword("oneOf");
15 ajv.addKeyword("oneOf", {
16 compile: function(schemas) {
17 return function(data) {
18 for (const schema of schemas) {
19 const validator = ajv.compile(schema);
20
21 // use clone data to prevent data modify
22 const testData = cloneDeep(data);
23 const ret = validator(testData);
24
25 if (ret) {
26 // modify data for removeAddtional
27 validator(data);
28 return ret;
29 }
30 }
31 return false;
32 };
33 },
34 modifying: true,
35 metaSchema: {
36 type: "array",
37 items: [{ type: "object" }],
38 },
39 errors: false,
40 });
41}
42
43/**
44 * override allOf keywords support removeAdditional option
45 * @param {import('ajv').Ajv} ajv
46 */
47function allOfRA(ajv) {
48 ajv.removeKeyword("allOf");
49 ajv.addKeyword("allOf", {
50 macro: function(schema) {
51 return merge({}, ...schema);
52 },
53 metaSchema: {
54 type: "array",
55 items: [{ type: "object" }],
56 },
57 errors: true,
58 });
59}
60
61const ajv = new Ajv({
62 coerceTypes: "array",
63 removeAdditional: "all", // strip any property not in openapi.yml
64 unknownFormats: ["int32"],
65});
66
67allOfRA(ajv);
68oneOfRA(ajv);
69
70const buildAjvErr = (errors = [], data) => ({
71 type: "validation",
72 details: errors
73 .filter(error => error.keyword !== "allOf")
74 .map(error => ({
75 keyword: error.keyword,
76 path: error.dataPath.replace(/^\./, ""),
77 message: error.message,
78 value: get(data, error.dataPath.replace(/^\./, "")),
79 })),
80});
81
82export default (reqSchema, resSchema) => {
83 return async (ctx, next) => {
84 if (reqSchema) {
85 const validateReq = ajv.compile(reqSchema);
86 const req = {
87 ...ctx.params,
88 body: ctx.request.body,
89 query: ctx.query,
90 headers: ctx.headers,
91 cookies: ctx.cookies,
92 };
93 if (!validateReq(req)) {
94 ctx.throw(
95 400,
96 `request is invalid`,
97 buildAjvErr(validateReq.errors, req) // return first error
98 );
99 }
100 debug("validate request success");
101 }
102
103 await next();
104
105 if (resSchema) {
106 const validateRes = ajv.compile(lowerCaseHeaders(resSchema));
107 const res = {
108 content: ctx.body,
109 headers: ctx.response.headers,
110 cookies: ctx.cookies,
111 };
112 if (!validateRes(res)) {
113 ctx.throw(500, buildAjvErr(validateRes.errors, res));
114 }
115 debug("validate response success");
116 }
117 };
118};
119
120/**
121 * 忽略 headers 大小写,schema 也转成小写
122 *
123 * @param {object} schema 原始 schema
124 * @returns {object} 供 ajv 校验使用的 schema
125 */
126function lowerCaseHeaders(schema) {
127 if (!schema.properties || !schema.properties.headers) return schema;
128 const headers = schema.properties.headers;
129 const properties = Object.keys(headers.properties).reduce(
130 (c, k) => ({ ...c, [k.toLowerCase()]: headers.properties[k] }),
131 {}
132 );
133 return {
134 ...schema,
135 properties: {
136 ...schema.properties,
137 headers: {
138 ...schema.headers,
139 properties,
140 },
141 },
142 };
143}