@wxn0brp/falcon-frame 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: Build
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ tags:
8
+ - "*"
9
+
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ build:
14
+ uses: wxn0brP/workflow-dist/.github/workflows/build-ts.yml@main
15
+ with:
16
+ scriptsHandling: "remove-all"
17
+ publishToNpm: true
18
+
19
+ secrets:
20
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wxn0brP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # FalconFrame
2
+
3
+ > Lightweight modular TypeScript web framework built for speed and clarity.
4
+
5
+ **FalconFrame** is a minimalist web framework inspired by the middleware-first style, with routing similar to Express, but written in pure TypeScript. It supports static files, data validation, and cookie management, all without external dependencies apart from a logger.
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
11
+ - 🚀 Zero-dependency routing engine
12
+ - 🧱 Middleware chaining
13
+ - 📁 Static file serving
14
+ - 📦 Request validation via inline schemas
15
+ - 🍪 Cookie management
16
+ - 🔍 Debuggable logger
17
+
18
+ ---
19
+
20
+ ## 📦 Installation
21
+
22
+ ```bash
23
+ yarn add github:wxn0brP/FalconFrame#dist
24
+ ```
25
+
26
+ ## 🚦 Usage Example
27
+
28
+ ```ts
29
+ import FalconFrame from "@wxn0brp/falcon-frame";
30
+ const app = new FalconFrame();
31
+
32
+ const USERS = {
33
+ admin: "hunter2",
34
+ };
35
+
36
+ // Global middleware
37
+ app.use((req, res, next) => {
38
+ console.log(`[${req.method}] ${req.path}`);
39
+ next();
40
+ });
41
+
42
+ // Middleware: auth check
43
+ const requireAuth = (req, res, next) => {
44
+ if (req.cookies.session === "admin") {
45
+ return next();
46
+ }
47
+ res.status(401).json({ error: "Unauthorized" });
48
+ };
49
+
50
+ // Static files
51
+ app.static("/", "public");
52
+
53
+ app.post("/login", (req, res) => {
54
+ const { valid, validErrors } = req.valid({
55
+ username: "required|string",
56
+ password: "required|string",
57
+ });
58
+
59
+ if (!valid) {
60
+ return res.status(400).json({ status: "error", errors: validErrors });
61
+ }
62
+
63
+ const { username, password } = req.body;
64
+ if (USERS[username] === password) {
65
+ res.cookie("session", username, { httpOnly: true });
66
+ return res.redirect("/dashboard");
67
+ }
68
+
69
+ res.status(401).json({ status: "fail", message: "Invalid credentials" });
70
+ });
71
+
72
+ app.post("/register", (req, res, next) => {
73
+ const { valid, validErrors } = req.valid({
74
+ username: "required|string|min:3|max:20",
75
+ password: "required|string|min:8",
76
+ });
77
+
78
+ if (!valid) {
79
+ return res.status(400).json({ status: "error", errors: validErrors });
80
+ }
81
+
82
+ next();
83
+ }, (req, res) => {
84
+ const { username, password } = req.body;
85
+ USERS[username] = password;
86
+ return { status: "success", message: "User registered successfully" };
87
+ });
88
+
89
+ // Protected route
90
+ app.get("/dashboard", requireAuth, (req, res) => {
91
+ return {
92
+ message: `👑 Welcome to the dashboard, ${req.cookies.session}`,
93
+ });
94
+ });
95
+
96
+ app.post("/logout", (req, res) => {
97
+ res.cookie("session", "", { maxAge: 0 });
98
+ res.json({ message: "👋 Logged out" });
99
+ });
100
+
101
+ // if no route matches
102
+ app.all("*", (req, res) => {
103
+ res.status(404).json({ error: "Not found" });
104
+ });
105
+
106
+ app.listen(3000, () => {
107
+ console.log("Server running on http://localhost:3000");
108
+ });
109
+ ```
110
+
111
+ ## 📜 License
112
+
113
+ Published under the MIT license
@@ -0,0 +1,5 @@
1
+ import { Body, Cookies, RouteHandler } from "./types.js";
2
+ export declare function parseCookies(cookieHeader: string): Cookies;
3
+ export declare function parseBody(contentType: string, body: string): Body;
4
+ export declare function getContentType(filePath: string): string;
5
+ export declare function handleStaticFiles(apiPath: string, dirPath: string): RouteHandler;
@@ -0,0 +1,73 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import querystring from "querystring";
4
+ export function parseCookies(cookieHeader) {
5
+ const cookies = {};
6
+ cookieHeader.split(";").forEach(cookie => {
7
+ const [name, ...valueParts] = cookie.split("=");
8
+ const value = decodeURIComponent(valueParts.join("=").trim());
9
+ cookies[name.trim()] = value;
10
+ });
11
+ return cookies;
12
+ }
13
+ export function parseBody(contentType, body) {
14
+ if (contentType.includes("application/json")) {
15
+ try {
16
+ return JSON.parse(body);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ else if (contentType.includes("application/x-www-form-urlencoded")) {
23
+ return querystring.parse(body);
24
+ }
25
+ return {};
26
+ }
27
+ export function getContentType(filePath) {
28
+ const ext = path.extname(filePath).toLowerCase();
29
+ switch (ext) {
30
+ case ".html":
31
+ return "text/html";
32
+ case ".css":
33
+ return "text/css";
34
+ case ".js":
35
+ return "application/javascript";
36
+ case ".json":
37
+ return "application/json";
38
+ case ".png":
39
+ return "image/png";
40
+ case ".jpg":
41
+ case ".jpeg":
42
+ return "image/jpeg";
43
+ case ".gif":
44
+ return "image/gif";
45
+ case ".svg":
46
+ return "image/svg+xml";
47
+ default:
48
+ return "application/octet-stream";
49
+ }
50
+ }
51
+ export function handleStaticFiles(apiPath, dirPath) {
52
+ return (req, res, next) => {
53
+ if (!req.path.startsWith(apiPath))
54
+ return next();
55
+ const filePath = path.join(dirPath, req.path.slice(apiPath.length));
56
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
57
+ res.setHeader("Content-Type", getContentType(filePath));
58
+ fs.createReadStream(filePath).pipe(res);
59
+ return true;
60
+ }
61
+ if (req.path.endsWith("/")) {
62
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
63
+ const indexPath = path.join(filePath, "index.html");
64
+ if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
65
+ res.setHeader("Content-Type", getContentType(indexPath));
66
+ fs.createReadStream(indexPath).pipe(res);
67
+ return true;
68
+ }
69
+ }
70
+ }
71
+ next();
72
+ };
73
+ }
@@ -0,0 +1,21 @@
1
+ import { Logger, LoggerOptions } from "@wxn0brp/wts-logger";
2
+ import http from "http";
3
+ import { FFResponse } from "./res.js";
4
+ import { FFRequest, Method, Middleware, RouteHandler } from "./types.js";
5
+ export declare class FalconFrame {
6
+ middlewares: Middleware[];
7
+ logger: Logger;
8
+ constructor(loggerOpts?: LoggerOptions);
9
+ addRoute(method: Method, path: string, ...handlers: RouteHandler[]): void;
10
+ use(path?: string | RouteHandler, middleware?: RouteHandler, method?: Method): void;
11
+ get(path: string, ...handlers: RouteHandler[]): void;
12
+ post(path: string, ...handlers: RouteHandler[]): void;
13
+ put(path: string, ...handlers: RouteHandler[]): void;
14
+ delete(path: string, ...handlers: RouteHandler[]): void;
15
+ all(path: string, ...handlers: RouteHandler[]): void;
16
+ static(apiPath: string, dirPath: string): void;
17
+ listen(port: number, callback?: () => void): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
18
+ getApp(): (req: any, res: any) => void;
19
+ }
20
+ export default FalconFrame;
21
+ export { FFResponse, FFRequest, RouteHandler, };
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ import { Logger } from "@wxn0brp/wts-logger";
2
+ import http from "http";
3
+ import { handleStaticFiles } from "./helpers.js";
4
+ import { handleRequest } from "./req.js";
5
+ import { FFResponse } from "./res.js";
6
+ import { FFRequest } from "./types.js";
7
+ export class FalconFrame {
8
+ middlewares = [];
9
+ logger;
10
+ constructor(loggerOpts) {
11
+ this.logger = new Logger({
12
+ loggerName: "falcon-frame",
13
+ ...loggerOpts
14
+ });
15
+ }
16
+ addRoute(method, path, ...handlers) {
17
+ const handler = handlers.pop();
18
+ handlers.forEach(middleware => this.use(path, middleware));
19
+ this.middlewares.push({ path, middleware: handler, method });
20
+ }
21
+ use(path = "/", middleware, method = "all") {
22
+ if (typeof path === "function") {
23
+ middleware = path;
24
+ path = "/";
25
+ }
26
+ this.middlewares.push({ path, middleware, method, use: true });
27
+ }
28
+ get(path, ...handlers) {
29
+ this.addRoute("get", path, ...handlers);
30
+ }
31
+ post(path, ...handlers) {
32
+ this.addRoute("post", path, ...handlers);
33
+ }
34
+ put(path, ...handlers) {
35
+ this.addRoute("put", path, ...handlers);
36
+ }
37
+ delete(path, ...handlers) {
38
+ this.addRoute("delete", path, ...handlers);
39
+ }
40
+ all(path, ...handlers) {
41
+ this.addRoute("all", path, ...handlers);
42
+ }
43
+ static(apiPath, dirPath) {
44
+ this.middlewares.push({
45
+ path: (apiPath + "/*").replace("//", "/"),
46
+ method: "get",
47
+ middleware: handleStaticFiles(apiPath, dirPath)
48
+ });
49
+ this.middlewares.push({
50
+ path: apiPath,
51
+ method: "get",
52
+ middleware: handleStaticFiles(apiPath, dirPath)
53
+ });
54
+ }
55
+ listen(port, callback) {
56
+ const server = http.createServer((req, res) => {
57
+ handleRequest(req, res, this);
58
+ });
59
+ server.listen(port, callback);
60
+ return server;
61
+ }
62
+ getApp() {
63
+ return (req, res) => {
64
+ handleRequest(req, res, this);
65
+ };
66
+ }
67
+ }
68
+ export default FalconFrame;
69
+ export { FFResponse, FFRequest, };
package/dist/req.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import FalconFrame from "./index.js";
2
+ import { FFResponse } from "./res.js";
3
+ import { FFRequest } from "./types.js";
4
+ export declare function handleRequest(req: FFRequest, res: FFResponse, FF: FalconFrame): void;
package/dist/req.js ADDED
@@ -0,0 +1,109 @@
1
+ import { URL } from "url";
2
+ import { parseBody, parseCookies } from "./helpers.js";
3
+ import { FFResponse } from "./res.js";
4
+ import { validate } from "./valid.js";
5
+ export function handleRequest(req, res, FF) {
6
+ Object.setPrototypeOf(res, FFResponse.prototype);
7
+ const originalEnd = res.end;
8
+ res.end = function (...any) {
9
+ res._ended = true;
10
+ return originalEnd.call(res, ...any);
11
+ };
12
+ const { logger, middlewares } = FF;
13
+ const parsedUrl = new URL(req.url || "", "http://localhost");
14
+ req.path = parsedUrl.pathname || "/";
15
+ req.query = Object.fromEntries(parsedUrl.searchParams);
16
+ req.cookies = parseCookies(req.headers.cookie || "");
17
+ req.params = {};
18
+ req.valid = (schema) => validate(schema, req.body);
19
+ logger.info(`Incoming request: ${req.method} ${req.url}`);
20
+ let body = "";
21
+ req.on("data", chunk => (body += chunk.toString()));
22
+ req.on("end", () => {
23
+ const contentType = req.headers["content-type"] || "";
24
+ req.body = parseBody(contentType, body);
25
+ logger.debug(`Request body: ${JSON.stringify(req.body)}`);
26
+ const matchedTypeMiddlewares = middlewares.filter(middleware => middleware.method === req.method.toLocaleLowerCase() || middleware.method === "all");
27
+ const matchedMiddlewares = matchMiddleware(req.path, matchedTypeMiddlewares);
28
+ if (matchedMiddlewares.length === 0) {
29
+ return res.status(404).end("404: File had second thoughts.");
30
+ }
31
+ logger.debug("Matched middlewares: " + matchedMiddlewares.map(middleware => middleware.path).join(", "));
32
+ let middlewareIndex = 0;
33
+ const next = async () => {
34
+ if (middlewareIndex >= matchedMiddlewares.length) {
35
+ return res.status(404).end("404: File had second thoughts");
36
+ }
37
+ const middleware = matchedMiddlewares[middlewareIndex++];
38
+ logger.debug(`Executing middleware ${middlewareIndex} of ${matchedMiddlewares.length} matched for path [${middleware.path}]`);
39
+ if (middleware.path.includes(":")) {
40
+ const middlewareParts = middleware.path.split("/");
41
+ const reqPathParts = req.path.split("/");
42
+ req.params = {};
43
+ for (let i = 0; i < middlewareParts.length; i++) {
44
+ if (middlewareParts[i].startsWith(":")) {
45
+ const paramName = middlewareParts[i].slice(1);
46
+ req.params[paramName] = reqPathParts[i];
47
+ }
48
+ }
49
+ }
50
+ const result = await middleware.middleware(req, res, next);
51
+ if (result && !res._ended) {
52
+ if (typeof result === "string") {
53
+ return res.end(result);
54
+ }
55
+ else if (typeof result === "object") {
56
+ return res.json(result);
57
+ }
58
+ }
59
+ };
60
+ next();
61
+ });
62
+ }
63
+ function matchMiddleware(url, middlewares) {
64
+ const matchedMiddlewares = [];
65
+ url = url.replace(/\/$/, "");
66
+ for (const middleware of middlewares) {
67
+ const cleanedMiddleware = middleware.path.replace(/\/$/, "");
68
+ if (middleware.use) {
69
+ if (url.startsWith(cleanedMiddleware)) {
70
+ matchedMiddlewares.push(middleware);
71
+ }
72
+ }
73
+ else if (cleanedMiddleware === "*") {
74
+ matchedMiddlewares.push(middleware);
75
+ }
76
+ else if (cleanedMiddleware.endsWith("/*")) {
77
+ const prefix = cleanedMiddleware.slice(0, -2);
78
+ if (url.startsWith(prefix)) {
79
+ matchedMiddlewares.push(middleware);
80
+ }
81
+ }
82
+ else if (cleanedMiddleware.includes(":")) {
83
+ const middlewareParts = cleanedMiddleware.split("/");
84
+ const urlParts = url.split("/");
85
+ if (middlewareParts.length !== urlParts.length) {
86
+ continue;
87
+ }
88
+ let matches = true;
89
+ for (let i = 0; i < middlewareParts.length; i++) {
90
+ if (middlewareParts[i].startsWith(":")) {
91
+ continue;
92
+ }
93
+ else if (middlewareParts[i] !== urlParts[i]) {
94
+ matches = false;
95
+ break;
96
+ }
97
+ }
98
+ if (matches) {
99
+ matchedMiddlewares.push(middleware);
100
+ }
101
+ }
102
+ else {
103
+ if (url === cleanedMiddleware) {
104
+ matchedMiddlewares.push(middleware);
105
+ }
106
+ }
107
+ }
108
+ return matchedMiddlewares;
109
+ }
package/dist/res.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import http from "http";
2
+ import { CookieOptions } from "./types.js";
3
+ export declare class FFResponse extends http.ServerResponse {
4
+ _ended: boolean;
5
+ json(data: any): void;
6
+ cookie(name: string, value: string, options?: CookieOptions): this;
7
+ status(code: number): this;
8
+ redirect(url: string): this;
9
+ sendFile(filePath: string): this;
10
+ }
package/dist/res.js ADDED
@@ -0,0 +1,39 @@
1
+ import http from "http";
2
+ import { getContentType } from "./helpers.js";
3
+ import { createReadStream } from "fs";
4
+ export class FFResponse extends http.ServerResponse {
5
+ _ended = false;
6
+ json(data) {
7
+ this.setHeader("Content-Type", "application/json");
8
+ this.end(JSON.stringify(data));
9
+ }
10
+ cookie(name, value, options = {}) {
11
+ let cookie = `${name}=${encodeURIComponent(value)}`;
12
+ if (options.maxAge)
13
+ cookie += `; Max-Age=${options.maxAge}`;
14
+ if (options.path)
15
+ cookie += `; Path=${options.path}`;
16
+ if (options.httpOnly)
17
+ cookie += `; HttpOnly`;
18
+ if (options.secure)
19
+ cookie += `; Secure`;
20
+ if (options.sameSite)
21
+ cookie += `; SameSite=${options.sameSite}`;
22
+ this.setHeader("Set-Cookie", cookie);
23
+ return this;
24
+ }
25
+ status(code) {
26
+ this.statusCode = code;
27
+ return this;
28
+ }
29
+ redirect(url) {
30
+ this.statusCode = 302;
31
+ this.setHeader("Location", url);
32
+ return this;
33
+ }
34
+ sendFile(filePath) {
35
+ this.setHeader("Content-Type", getContentType(filePath));
36
+ createReadStream(filePath).pipe(this);
37
+ return this;
38
+ }
39
+ }
package/dist/test.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/test.js ADDED
@@ -0,0 +1,60 @@
1
+ import FalconFrame from "./index.js";
2
+ const app = new FalconFrame({
3
+ logLevel: "INFO",
4
+ });
5
+ app.use((req, res, next) => {
6
+ console.log(`[${req.method}] ${req.path}`);
7
+ next();
8
+ });
9
+ app.static("/", "public");
10
+ app.get("/hello", (req, res) => {
11
+ const name = req.query.name || "World";
12
+ res.json({
13
+ message: `Hello, ${name}?`,
14
+ query: req.query,
15
+ });
16
+ });
17
+ app.get("/hello/*", (req, res) => {
18
+ res.json({
19
+ message: `Hello, ${req.params.name}!`,
20
+ query: req.query,
21
+ });
22
+ });
23
+ app.get("/greet/:name", (req, res, next) => {
24
+ console.log(req.params);
25
+ next();
26
+ }, (req, res) => {
27
+ res.json({
28
+ message: `Hello, ${req.params.name}!`,
29
+ });
30
+ });
31
+ app.post("/submit", (req, res, next) => {
32
+ const { validErrors, valid } = req.valid({
33
+ login: "required|string",
34
+ password: "required|string|min:8",
35
+ });
36
+ if (!valid) {
37
+ res.status(400).json({
38
+ status: "error",
39
+ errors: validErrors,
40
+ });
41
+ }
42
+ else
43
+ next();
44
+ }, async (req, res) => {
45
+ console.log("run");
46
+ res.redirect("/hello?name=" + req.body.login);
47
+ return {
48
+ status: "success",
49
+ data: `Hello ${req.body.login}`,
50
+ };
51
+ });
52
+ app.use((req, res) => {
53
+ res.status(404);
54
+ res.json({
55
+ message: "Not found",
56
+ });
57
+ });
58
+ app.listen(3000, () => {
59
+ console.log("Server running on http://localhost:3000");
60
+ });
@@ -0,0 +1,46 @@
1
+ import { FFResponse } from "./res.js";
2
+ import http from "http";
3
+ export type RouteHandler = (req: FFRequest, res: FFResponse, next?: () => void) => void | any;
4
+ export type Method = "get" | "post" | "put" | "delete" | "all";
5
+ export interface Params {
6
+ [key: string]: string;
7
+ }
8
+ export interface Cookies {
9
+ [key: string]: string;
10
+ }
11
+ export interface Query {
12
+ [key: string]: string | string[];
13
+ }
14
+ export interface Body {
15
+ [key: string]: any;
16
+ }
17
+ export declare class FFRequest extends http.IncomingMessage {
18
+ path: string;
19
+ query: Query;
20
+ params: Params;
21
+ cookies: Cookies;
22
+ body: Body;
23
+ valid: (schema: ValidationSchema) => ValidationResult;
24
+ }
25
+ export interface Middleware {
26
+ path: string;
27
+ method: Method;
28
+ middleware: RouteHandler;
29
+ use?: true;
30
+ }
31
+ export interface CookieOptions {
32
+ maxAge?: number;
33
+ path?: string;
34
+ httpOnly?: boolean;
35
+ secure?: boolean;
36
+ sameSite?: "Strict" | "Lax" | "None";
37
+ }
38
+ export interface ValidationSchema {
39
+ [key: string]: string;
40
+ }
41
+ export interface ValidationResult {
42
+ valid: boolean;
43
+ validErrors: {
44
+ [key: string]: string[];
45
+ };
46
+ }
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ import http from "http";
2
+ export class FFRequest extends http.IncomingMessage {
3
+ path;
4
+ query;
5
+ params;
6
+ cookies;
7
+ body;
8
+ valid;
9
+ }
@@ -0,0 +1,2 @@
1
+ import { ValidationResult, ValidationSchema } from "./types.js";
2
+ export declare function validate(schema: ValidationSchema, data: any): ValidationResult;
package/dist/valid.js ADDED
@@ -0,0 +1,56 @@
1
+ export function validate(schema, data) {
2
+ const errors = {};
3
+ let isValid = true;
4
+ for (const key in schema) {
5
+ const rules = schema[key].split("|");
6
+ const value = data[key];
7
+ const fieldErrors = [];
8
+ for (const rule of rules) {
9
+ const [ruleName, param] = rule.split(":");
10
+ switch (ruleName) {
11
+ case "required":
12
+ if (!value && value !== 0) {
13
+ fieldErrors.push(`${key} is required`);
14
+ }
15
+ break;
16
+ case "string":
17
+ if (typeof value !== "string") {
18
+ fieldErrors.push(`${key} must be a string`);
19
+ }
20
+ break;
21
+ case "number":
22
+ if (typeof value !== "number") {
23
+ fieldErrors.push(`${key} must be a number`);
24
+ }
25
+ break;
26
+ case "min":
27
+ if (typeof value === "string" && value.length < parseInt(param)) {
28
+ fieldErrors.push(`${key} must be at least ${param} characters long`);
29
+ }
30
+ if (typeof value === "number" && value < parseInt(param)) {
31
+ fieldErrors.push(`${key} must be at least ${param}`);
32
+ }
33
+ break;
34
+ case "max":
35
+ if (typeof value === "string" && value.length > parseInt(param)) {
36
+ fieldErrors.push(`${key} must not exceed ${param} characters`);
37
+ }
38
+ if (typeof value === "number" && value > parseInt(param)) {
39
+ fieldErrors.push(`${key} must not exceed ${param}`);
40
+ }
41
+ break;
42
+ case "email":
43
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
44
+ if (!emailRegex.test(value)) {
45
+ fieldErrors.push(`${key} must be a valid email`);
46
+ }
47
+ break;
48
+ }
49
+ }
50
+ if (fieldErrors.length > 0) {
51
+ isValid = false;
52
+ errors[key] = fieldErrors;
53
+ }
54
+ }
55
+ return { valid: isValid, validErrors: errors };
56
+ }