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:
2025-12-20 18:19:03 +01:00
parent 18efd445e5
commit 7e1eaf3f1f
8 changed files with 118 additions and 42 deletions

View File

@@ -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