Add dynamic route matching and validation improvements
Introduces dynamic route parameter support in route path computation and schema matching. Adds a utility for sending validation errors, updates schema definitions to support header validation, and refactors server middleware to handle header, body, and query validation with improved error handling. Also adds a placeholder user route and updates dependencies to include 'path-to-regexp'.
This commit is contained in:
88
server.js
88
server.js
@@ -3,7 +3,10 @@ const app = express()
|
||||
const path = require("node:path")
|
||||
const Logger = require("./modules/logger")
|
||||
const logger = Logger.createLogger(__dirname)
|
||||
const utils = require("./modules/utils")
|
||||
const loader = require("./modules/loader")
|
||||
const DefaultError = require("./errors/DefaultError")
|
||||
const path2regex = require("path-to-regexp")
|
||||
|
||||
const routes = loader.getRecursiveFiles(path.join(__dirname, "routes"))
|
||||
const schemas = loader.getRecursiveFiles(path.join(__dirname, "schemas"))
|
||||
@@ -35,46 +38,69 @@ app.all(/.*/, (req, res, next) => {
|
||||
currentPath = currentPath.slice(0, -1)
|
||||
}
|
||||
|
||||
const schemaConfig = schemaRegistry[currentPath]
|
||||
let schemaConfig = schemaRegistry[currentPath]
|
||||
let matchedParams = {}
|
||||
|
||||
if (!schemaConfig) {
|
||||
const registeredRoutes = Object.keys(schemaRegistry)
|
||||
for (const routePattern of registeredRoutes) {
|
||||
if (!routePattern.includes(":")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const matcher = path2regex.match(routePattern, { decode: decodeURIComponent })
|
||||
const result = matcher(currentPath)
|
||||
|
||||
if (result) {
|
||||
schemaConfig = schemaRegistry[routePattern]
|
||||
matchedParams = result.params
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!schemaConfig || !schemaConfig[req.method]) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const methodConfig = schemaConfig[req.method]
|
||||
const zodSchema = methodConfig.zod || methodConfig
|
||||
const errorConfig = methodConfig.error || { status: 400, message: "Validation Error" }
|
||||
const dataToValidate = (req.method === "GET" || req.method === "DELETE") ? req.query : req.body
|
||||
const result = zodSchema.safeParse(dataToValidate)
|
||||
req.params = { ...req.params, ...matchedParams }
|
||||
|
||||
if (result.success) {
|
||||
if (req.method === "GET" || req.method === "DELETE") {
|
||||
const methodConfig = schemaConfig[req.method]
|
||||
const errorConfig = methodConfig.error || { status: 400, message: "Validation Error" }
|
||||
|
||||
if (methodConfig.headers) {
|
||||
const headerResult = methodConfig.headers.safeParse(req.headers)
|
||||
if (!headerResult.success) {
|
||||
return utils.sendValidationError(req, res, headerResult, "headers", currentPath, errorConfig)
|
||||
}
|
||||
}
|
||||
|
||||
let dataSchema = null
|
||||
let dataToValidate = null
|
||||
let validationType = "body"
|
||||
|
||||
if (req.method === "GET" || req.method === "DELETE") {
|
||||
dataSchema = methodConfig.query
|
||||
dataToValidate = req.query
|
||||
validationType = "query"
|
||||
} else {
|
||||
dataSchema = methodConfig.body
|
||||
dataToValidate = req.body
|
||||
}
|
||||
|
||||
if (dataSchema) {
|
||||
const result = dataSchema.safeParse(dataToValidate)
|
||||
if (!result.success) {
|
||||
return utils.sendValidationError(req, res, result, validationType, currentPath, errorConfig)
|
||||
}
|
||||
if (validationType === "query") {
|
||||
req.query = result.data
|
||||
} else {
|
||||
req.body = result.data
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
||||
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress
|
||||
logger.warn(`Validation failed for ${req.method.cyan} ${currentPath.cyan.bold} ` + `<IP:${ip}>`.bold, ["WEB", "yellow"])
|
||||
|
||||
const response = {
|
||||
success: false,
|
||||
message: errorConfig.message,
|
||||
errors: result.error.issues.map(e => ({
|
||||
field: e.path.join("."),
|
||||
message: e.message
|
||||
}))
|
||||
}
|
||||
|
||||
if (methodConfig.error) {
|
||||
const extras = { ...methodConfig.error }
|
||||
delete extras.status
|
||||
delete extras.message
|
||||
Object.assign(response, extras)
|
||||
}
|
||||
|
||||
return res.status(errorConfig.status || 400).json(response)
|
||||
return next()
|
||||
})
|
||||
|
||||
for (const route of routes) {
|
||||
@@ -96,6 +122,10 @@ for (const route of routes) {
|
||||
}
|
||||
}
|
||||
|
||||
app.all(/.*/, (req, res, next) => {
|
||||
next(new DefaultError(404, `Can't find ${req.originalUrl} on this server!`, null, "NotFound"))
|
||||
})
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
const statusCode = err.statusCode || 500
|
||||
|
||||
|
||||
Reference in New Issue
Block a user