Абстракция для валидации параметров запроса
~/utils/express-validation-middleware.ts
import {
Request as IRequest,
Response as IResponse,
NextFunction as INextFunction,
} from 'express'
import { THelp } from './interfaces'
// NOTE: Соглашение facebook like
// Бэк присылает ответ в виде { ok: boolean; message?: string; <Возможно, чо-то еще> }
type TProps = {
rules: THelp
}
export const withReqParamsValidationMW = ({ rules }: TProps) =>
(req: IRequest, res: IResponse, next: INextFunction) => {
// -- NOTE: Errs handler
const errs: { msg: string, _reponseDetails?: any }[] = []
for (const reqProp in rules.params) {
switch (reqProp) {
case 'body':
case 'query':
for (const key in rules.params[reqProp]) {
if (rules.params[reqProp][key]?.required && !req[reqProp][key]) {
const validationResult = rules.params[reqProp][key]?.validate(req[reqProp][key])
const errOpts: any = {
msg: `Missing required param: \`req.${reqProp}.${key}\` (${rules.params[reqProp][key].type}: ${rules.params[reqProp][key].descr})${!!validationResult.reason ? ` | ⚠️ By developer: ${validationResult.reason}` : ''}`
}
if (!!validationResult._reponseDetails)
errOpts._reponseDetails = validationResult._reponseDetails
errs.push(errOpts)
} else {
// -- NOTE: Если имеется необязательный параметр, проверим его
if (!!req[reqProp][key]) {
try {
const validationResult = rules.params[reqProp][key]?.validate(req[reqProp][key])
if (!validationResult.ok) {
const errOpts: {
msg: string;
_reponseDetails?: {
status: number;
[key: string]: any;
}
} = {
msg: `Incorrect request param format: \`req.${reqProp}.${key}\` (${rules.params[reqProp][key].descr}) expected: ${rules.params[reqProp][key].type}. Received: ${typeof req[reqProp][key]}${!!validationResult.reason ? ` | ⚠️ By developer: ${validationResult.reason}` : ''}`,
}
if (!!validationResult._reponseDetails)
errOpts._reponseDetails = validationResult._reponseDetails
errs.push(errOpts)
}
} catch (err) {
errs.push({
msg: `Не удалось проверить поле: \`req.${reqProp}.${key}\` (${rules.params[reqProp][key].descr}); ${typeof err === 'string' ? err : (err.message || 'No err.message')}`
})
}
}
// --
}
}
break
default:
break
}
}
if (errs.length > 0) {
let status = 400 // NOTE: Or anything by default?
const result: any = {
ok: false,
message: `⛔ ERR! ${errs.map(({ msg }) => msg).join('; ')}`,
_service: {
originalBody: req.body,
originalQuery: req.query,
rules,
}
}
// -- NOTE: Пробуем добавить детали первой ошибки результатов обработки в ответ (если они есть)
try {
// NOTE: v1 Добавить, если они имеются только у превой ошибки
// if (!!errs[0]._reponseDetails) {
// const { status: newStatus, _addProps } = errs[0]._reponseDetails
// status = newStatus
// if (!!_addProps && Object.keys(_addProps).length > 0) {
// for (const key in _addProps) result[key] = _addProps[key]
// }
// }
// NOTE: v2 А если для этой нет? Нужно ли добавить детали хотя бы для одной ошибки из имеющихся?
let _reponseDetails: any = null
for (const err of errs) {
if (!!err._reponseDetails) {
_reponseDetails = err._reponseDetails
break
}
}
if (!!_reponseDetails) {
const { status: newStatus, _addProps } = _reponseDetails
status = newStatus
if (!!_addProps && Object.keys(_addProps).length > 0) {
for (const key in _addProps) result[key] = _addProps[key]
}
}
} catch (err) {
result._service.message = typeof err === 'string' ? err : (err.message || 'No err.message')
}
// --
return res.status(status).send(result)
}
// --
return next()
}
~/utils/types.ts
export type TValidationResult = {
ok: boolean;
reason?: string;
_reponseDetails?: {
status: number;
_addProps?: {
[key: string]: any;
}
}
}
export type THelp = {
params: {
body?: {
[key: string]: {
type: string; // NOTE: Это просто для информации (не строка с типом js)
descr: string;
required: boolean;
validate: (val: any) => TValidationResult;
}
}
query?: {
[key: string]: {
type: string;
descr: string;
required: boolean;
validate: (val: any) => TValidationResult;
}
}
}
res?: {
[key: string]: any;
}
}
route-example.ts
import { TValidationResult } from './types'
export const rules = {
params: {
body: {
// NOTE: Перечисляем поля в теле ответа в качестве ключей...
text: {
type: 'string',
descr: 'Target input text',
required: true,
validate: (val: any) => {
const result: TValidationResult = {
ok: true,
}
switch (true) {
case !val || typeof val !== 'string':
result.ok = false
result.reason = 'Ожидается непустая строка'
break
default:
break
}
result._reponseDetails = {
status: 200,
}
return result
}
}
}
}
}
export const getSlugify = (req: IRequest, res: IResponse) => {
const { text } = req.body
const _result: any = { ok: true }
try {
// @ts-ignore
_result.result = slugify(text)
return res.status(200).json({ ok: true, ..._result })
} catch (err) {
return res.status(200).json({ ok: false, message: err?.message || 'No err.message' })
}
}
Usage example
import express, { Express as IExpress } from 'express'
import { getSlugify, rules as getSlugifyRules } from './route-example'
import { withReqParamsValidationMW } from '~/utils/express-validation-middleware'
const careServiceApi: IExpress = express()
careServiceApi.post(
'/get-slugify',
withReqParamsValidationMW({ rules: getSlugifyRules }),
getSlugify,
)
export { careServiceApi }
// NOTE: POST /get-slugify { text: 'Example' } -> { ok: true, <Что-то еще> }
// NOTE: POST /get-slugify { text: '' } -> { ok: false, message: '⛔ ERR! Incorrect request param format: `req.body.text` (Target input text) expected: string. Received: string | ⚠️ By developer: Ожидается непустая строка' }