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:
parent
18efd445e5
commit
7e1eaf3f1f
@ -6,6 +6,8 @@ class DefaultError extends Error {
|
||||
this.cause = cause || "Internal Server Error"
|
||||
this.message = message || "Internal Server Error"
|
||||
this.isOperational = true
|
||||
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
|
||||
@ -28,20 +28,19 @@ function getRecursiveFiles(dir) {
|
||||
|
||||
function computeRoutePath(baseDir, filePath) {
|
||||
const relativePath = path.relative(baseDir, filePath)
|
||||
const normalizedPath = relativePath.replace(/\\/g, "/")
|
||||
let route = "/" + relativePath.split(path.sep).join("/")
|
||||
|
||||
let route = "/" + normalizedPath
|
||||
|
||||
if (route.endsWith("index.js")) {
|
||||
route = route.replace("index.js", "")
|
||||
} else {
|
||||
route = route.replace(".js", "")
|
||||
if (route.endsWith(".js")) {
|
||||
route = route.slice(0, -3)
|
||||
}
|
||||
if (route.endsWith("/index")) {
|
||||
route = route.slice(0, -6)
|
||||
}
|
||||
|
||||
route = route.replace(/\/{2,}/g, "/")
|
||||
route = route.replace(/\[([^\]]+)\]/g, ":$1")
|
||||
|
||||
if (route.length > 1 && route.endsWith('/')) {
|
||||
route = route.slice(0, -1)
|
||||
if (route === "") {
|
||||
return "/"
|
||||
}
|
||||
|
||||
return route
|
||||
|
||||
30
modules/utils.js
Normal file
30
modules/utils.js
Normal file
@ -0,0 +1,30 @@
|
||||
const path = require("node:path")
|
||||
const Logger = require("./logger")
|
||||
const logger = Logger.createLogger(path.join(__dirname, ".."))
|
||||
|
||||
function sendValidationError(req, res, zodResult, type, path, errorConfig) {
|
||||
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress
|
||||
logger.warn(`Validation failed for ${req.method.cyan} ${path.cyan.bold} (${type}) ` + `<IP:${ip}>`.bold, ["WEB", "yellow"])
|
||||
|
||||
const response = {
|
||||
success: false,
|
||||
message: errorConfig.message || `Validation failed in ${type}`,
|
||||
errors: zodResult.error.issues.map(e => ({
|
||||
field: e.path.join("."),
|
||||
message: e.message
|
||||
}))
|
||||
}
|
||||
|
||||
if (errorConfig) {
|
||||
const extras = { ...errorConfig }
|
||||
delete extras.status
|
||||
delete extras.message
|
||||
Object.assign(response, extras)
|
||||
}
|
||||
|
||||
return res.status(errorConfig.status || 400).json(response)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendValidationError
|
||||
}
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"path-to-regexp": "^8.3.0",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"path-to-regexp": "^8.3.0",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
8
routes/users/[id].js
Normal file
8
routes/users/[id].js
Normal file
@ -0,0 +1,8 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
router.post("/", async (req, res, next) => {
|
||||
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -1,9 +1,14 @@
|
||||
const z = require("zod")
|
||||
|
||||
module.exports = {
|
||||
errorFormat: "default",
|
||||
POST: {
|
||||
zod: z.object({
|
||||
headers: z.object({
|
||||
authorization: z.string()
|
||||
.startsWith("Bearer ", { message: "Token d'authentification manquant ou invalide." })
|
||||
.optional(),
|
||||
"content-type": z.string().regex(/application\/json/i).optional()
|
||||
}),
|
||||
body: z.object({
|
||||
email: z.string()
|
||||
.email({ message: "Invalid E-Mail format." })
|
||||
.toLowerCase(),
|
||||
@ -17,7 +22,7 @@ module.exports = {
|
||||
}),
|
||||
error: {
|
||||
code: 422,
|
||||
message: "Invalid body request"
|
||||
message: "Invalid request data"
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user