@wxn0brp/falcon-frame 0.8.7 → 0.9.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.
- package/dist/body-utils.d.ts +2 -3
- package/dist/body-utils.js +13 -29
- package/dist/body.d.ts +8 -3
- package/dist/body.js +9 -7
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +4 -6
- package/dist/helpers.d.ts +2 -1
- package/dist/helpers.js +13 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +52 -12
- package/dist/req.js +40 -16
- package/dist/res.d.ts +1 -0
- package/dist/res.js +8 -2
- package/dist/router.js +2 -0
- package/dist/types.d.ts +18 -4
- package/dist/types.js +3 -0
- package/dist/valid.d.ts +8 -2
- package/dist/valid.js +118 -7
- package/package.json +2 -2
package/dist/body-utils.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { FFResponse } from "./
|
|
2
|
-
import type { FFRequest
|
|
1
|
+
import { FFResponse } from "./res.js";
|
|
2
|
+
import type { FFRequest } from "./types.js";
|
|
3
3
|
export declare function parseLimit(limit: string | number): number;
|
|
4
4
|
export declare function getContentType(req: FFRequest): string | undefined;
|
|
5
5
|
export declare function getRawBody(req: FFRequest, res: FFResponse, limit: number): Promise<string>;
|
|
6
|
-
export declare function getStandardBodyParser(type: string, parser: ParseBodyFunction, opts: StandardBodyParserOptions): RouteHandler;
|
package/dist/body-utils.js
CHANGED
|
@@ -5,10 +5,12 @@ export function parseLimit(limit) {
|
|
|
5
5
|
return limit;
|
|
6
6
|
if (typeof limit !== "string")
|
|
7
7
|
return 0;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const match = limit
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/b$/, "")
|
|
11
|
+
.match(/^(\d+)([kmg])?$/);
|
|
10
12
|
if (!match)
|
|
11
|
-
|
|
13
|
+
throw new Error(`Invalid body limit: ${limit}`);
|
|
12
14
|
const num = parseInt(match[1], 10);
|
|
13
15
|
const unit = match[2]?.toLowerCase();
|
|
14
16
|
switch (unit) {
|
|
@@ -27,42 +29,24 @@ export function getContentType(req) {
|
|
|
27
29
|
}
|
|
28
30
|
export function getRawBody(req, res, limit) {
|
|
29
31
|
return new Promise((resolve, reject) => {
|
|
30
|
-
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let total = 0;
|
|
31
34
|
req.on("data", chunk => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
36
|
+
total += buf.length;
|
|
37
|
+
if (limit && total > limit) {
|
|
35
38
|
res.status(413);
|
|
36
39
|
res.FF._413(req, res);
|
|
37
40
|
req.destroy();
|
|
38
|
-
return reject(
|
|
41
|
+
return reject(new Error("Payload Too Large"));
|
|
39
42
|
}
|
|
43
|
+
chunks.push(buf);
|
|
40
44
|
});
|
|
41
45
|
req.on("end", () => {
|
|
42
|
-
resolve(
|
|
46
|
+
resolve(Buffer.concat(chunks).toString());
|
|
43
47
|
});
|
|
44
48
|
req.on("error", err => {
|
|
45
49
|
reject(err);
|
|
46
50
|
});
|
|
47
51
|
});
|
|
48
52
|
}
|
|
49
|
-
export function getStandardBodyParser(type, parser, opts) {
|
|
50
|
-
const limit = parseLimit(opts.limit || "100k");
|
|
51
|
-
return async (req, res, next) => {
|
|
52
|
-
if ((typeof req.body === "object" && Object.keys(req.body).length) ||
|
|
53
|
-
getContentType(req) !== type) {
|
|
54
|
-
return next();
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
const body = await getRawBody(req, res, limit);
|
|
58
|
-
req.body = await parser(body, req, res);
|
|
59
|
-
next();
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
if (res._ended)
|
|
63
|
-
return;
|
|
64
|
-
req.body = {};
|
|
65
|
-
next();
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
}
|
package/dist/body.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare
|
|
3
|
-
export declare
|
|
1
|
+
import { ParseBodyFunction } from "./types.js";
|
|
2
|
+
export declare const json: ParseBodyFunction;
|
|
3
|
+
export declare const urlencoded: ParseBodyFunction;
|
|
4
|
+
export declare const yaml: ParseBodyFunction;
|
|
5
|
+
export declare const toml: ParseBodyFunction;
|
|
6
|
+
export declare const json5: ParseBodyFunction;
|
|
7
|
+
export declare const xml: ParseBodyFunction;
|
|
8
|
+
export declare const text: ParseBodyFunction;
|
package/dist/body.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import querystring from "querystring";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
export const json = body => JSON.parse(body);
|
|
3
|
+
export const urlencoded = body => querystring.parse(body);
|
|
4
|
+
export const yaml = body => Bun.YAML.parse(body);
|
|
5
|
+
export const toml = body => Bun.TOML.parse(body);
|
|
6
|
+
export const json5 = body => Bun.JSON5.parse(body);
|
|
7
|
+
export const xml = body => new DOMParser().parseFromString(body, "text/xml");
|
|
8
|
+
export const text = body => ({
|
|
9
|
+
data: body,
|
|
10
|
+
});
|
package/dist/debug.d.ts
CHANGED
package/dist/debug.js
CHANGED
|
@@ -5,7 +5,7 @@ export function createDebug(opts = {}) {
|
|
|
5
5
|
"localhost",
|
|
6
6
|
"127.0.0.1",
|
|
7
7
|
"::1",
|
|
8
|
-
],
|
|
8
|
+
], } = opts;
|
|
9
9
|
if (process.env.NODE_ENV === "production" &&
|
|
10
10
|
!process.env.FF_DEBUG_PRODUCTION_ENABLED)
|
|
11
11
|
return (req, res, next) => {
|
|
@@ -24,14 +24,14 @@ export function createDebug(opts = {}) {
|
|
|
24
24
|
return isHostAllowed(req) && checkAuth(req);
|
|
25
25
|
}
|
|
26
26
|
const router = new Router();
|
|
27
|
-
router.get(url, (req, res
|
|
27
|
+
router.get(url, (req, res) => {
|
|
28
28
|
if (!isAllowed(req))
|
|
29
|
-
return
|
|
29
|
+
return req.FF._404(req, res);
|
|
30
30
|
res.sendFile(join(import.meta.dirname, "debug.html"));
|
|
31
31
|
});
|
|
32
32
|
const sse = router.sse(url + "/sse", (req, res, next) => {
|
|
33
33
|
if (!isAllowed(req))
|
|
34
|
-
return req.FF.
|
|
34
|
+
return req.FF._404(req, res);
|
|
35
35
|
next();
|
|
36
36
|
}, () => { });
|
|
37
37
|
router.use("/", (req, res, next) => {
|
|
@@ -54,7 +54,5 @@ export function createDebug(opts = {}) {
|
|
|
54
54
|
};
|
|
55
55
|
next();
|
|
56
56
|
});
|
|
57
|
-
if (logs)
|
|
58
|
-
router.post("/logs", () => "ok");
|
|
59
57
|
return router;
|
|
60
58
|
}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Cookies } from "./types.js";
|
|
1
|
+
import { Cookies, FFRequest } from "./types.js";
|
|
2
2
|
export declare function parseCookies(cookieHeader: string): Cookies;
|
|
3
|
+
export declare function getIP(req: FFRequest): string;
|
|
3
4
|
export declare function getContentType(filePath: string, utf8?: boolean): string;
|
|
4
5
|
export declare const ensureLeadingDot: (str: string) => string;
|
package/dist/helpers.js
CHANGED
|
@@ -8,11 +8,24 @@ export function parseCookies(cookieHeader) {
|
|
|
8
8
|
});
|
|
9
9
|
return cookies;
|
|
10
10
|
}
|
|
11
|
+
export function getIP(req) {
|
|
12
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
13
|
+
if (forwarded) {
|
|
14
|
+
return (typeof forwarded === "string" ? forwarded : forwarded[0])
|
|
15
|
+
.split(",")[0]
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
return req.socket.remoteAddress || "";
|
|
19
|
+
}
|
|
11
20
|
const mimeTypes = {
|
|
12
21
|
".html": "text/html",
|
|
13
22
|
".css": "text/css",
|
|
14
23
|
".js": "application/javascript",
|
|
15
24
|
".json": "application/json",
|
|
25
|
+
".json5": "application/json5",
|
|
26
|
+
".yaml": "application/yaml",
|
|
27
|
+
".yml": "application/yaml",
|
|
28
|
+
".toml": "application/toml",
|
|
16
29
|
".png": "image/png",
|
|
17
30
|
".jpg": "image/jpeg",
|
|
18
31
|
".jpeg": "image/jpeg",
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ import http from "http";
|
|
|
3
3
|
import { renderHTML } from "./render.js";
|
|
4
4
|
import { FFResponse } from "./res.js";
|
|
5
5
|
import { Router } from "./router.js";
|
|
6
|
-
import type { BeforeHandleRequest, CombinedVars, EngineCallback, ErrorHandler, FFOpts, FFRequest, RouteHandler, ValidationErrorFormatter } from "./types.js";
|
|
6
|
+
import type { BeforeHandleRequest, BodyParserEntry, CombinedVars, EngineCallback, ErrorHandler, FFOpts, FFRequest, ParseBodyFunction, RouteHandler, StandardBodyParserOptions, ValidationErrorFormatter } from "./types.js";
|
|
7
7
|
export declare class FalconFrame<Vars extends Record<string, any> = {}> extends Router {
|
|
8
8
|
logger: Logger;
|
|
9
|
-
bodyParsers:
|
|
9
|
+
bodyParsers: Record<string, BodyParserEntry>;
|
|
10
10
|
vars: CombinedVars<Vars>;
|
|
11
11
|
opts: FFOpts;
|
|
12
12
|
engines: Record<string, EngineCallback>;
|
|
@@ -15,7 +15,7 @@ export declare class FalconFrame<Vars extends Record<string, any> = {}> extends
|
|
|
15
15
|
_413: RouteHandler;
|
|
16
16
|
_500: ErrorHandler;
|
|
17
17
|
constructor(opts?: Partial<FFOpts>);
|
|
18
|
-
addBodyParser(parser:
|
|
18
|
+
addBodyParser(contentType: string, parser: ParseBodyFunction, opts?: StandardBodyParserOptions): this;
|
|
19
19
|
listen(port: number | string, callback?: (() => void) | boolean, beforeHandleRequest?: BeforeHandleRequest): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
20
20
|
getApp(beforeHandleRequest?: BeforeHandleRequest): (req: any, res: any) => Promise<void>;
|
|
21
21
|
engine(ext: string, callback: EngineCallback): this;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Logger } from "@wxn0brp/lucerna-log";
|
|
2
2
|
import http from "http";
|
|
3
|
-
import
|
|
3
|
+
import * as bodyParser from "./body.js";
|
|
4
|
+
import { parseLimit } from "./body-utils.js";
|
|
4
5
|
import { createCORS } from "./cors.js";
|
|
5
6
|
import { renderHTML } from "./render.js";
|
|
6
7
|
import { handleRequest } from "./req.js";
|
|
@@ -8,7 +9,7 @@ import { FFResponse } from "./res.js";
|
|
|
8
9
|
import { Router } from "./router.js";
|
|
9
10
|
export class FalconFrame extends Router {
|
|
10
11
|
logger;
|
|
11
|
-
bodyParsers =
|
|
12
|
+
bodyParsers = {};
|
|
12
13
|
vars = {};
|
|
13
14
|
opts = {};
|
|
14
15
|
engines = {};
|
|
@@ -39,14 +40,50 @@ export class FalconFrame extends Router {
|
|
|
39
40
|
bodyLimit: "10m",
|
|
40
41
|
...opts,
|
|
41
42
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
const dp = this.opts.disableParser || {};
|
|
44
|
+
const isBun = !!globalThis.Bun;
|
|
45
|
+
const bunOnly = new Set([
|
|
46
|
+
"json5",
|
|
47
|
+
"yaml",
|
|
48
|
+
"toml",
|
|
49
|
+
]);
|
|
50
|
+
const parsers = {
|
|
51
|
+
json: {
|
|
52
|
+
fn: bodyParser.json,
|
|
53
|
+
ct: "application/json",
|
|
54
|
+
},
|
|
55
|
+
urlencoded: {
|
|
56
|
+
fn: bodyParser.urlencoded,
|
|
57
|
+
ct: "application/x-www-form-urlencoded",
|
|
58
|
+
},
|
|
59
|
+
json5: {
|
|
60
|
+
fn: bodyParser.json5,
|
|
61
|
+
ct: "application/json5",
|
|
62
|
+
},
|
|
63
|
+
yaml: {
|
|
64
|
+
fn: bodyParser.yaml,
|
|
65
|
+
ct: "application/yaml",
|
|
66
|
+
},
|
|
67
|
+
toml: {
|
|
68
|
+
fn: bodyParser.toml,
|
|
69
|
+
ct: "application/toml",
|
|
70
|
+
},
|
|
71
|
+
xml: {
|
|
72
|
+
fn: bodyParser.xml,
|
|
73
|
+
ct: "application/xml",
|
|
74
|
+
},
|
|
75
|
+
text: {
|
|
76
|
+
fn: bodyParser.text,
|
|
77
|
+
ct: "text/plain",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
for (const [key, { fn, ct }] of Object.entries(parsers)) {
|
|
81
|
+
if (dp[key])
|
|
82
|
+
continue;
|
|
83
|
+
if (bunOnly.has(key) && !isBun)
|
|
84
|
+
continue;
|
|
85
|
+
this.addBodyParser(ct, fn);
|
|
86
|
+
}
|
|
50
87
|
this.engine(".html", (path, data, callback, FF) => {
|
|
51
88
|
try {
|
|
52
89
|
const content = renderHTML({
|
|
@@ -61,8 +98,11 @@ export class FalconFrame extends Router {
|
|
|
61
98
|
}
|
|
62
99
|
});
|
|
63
100
|
}
|
|
64
|
-
addBodyParser(parser) {
|
|
65
|
-
this.bodyParsers
|
|
101
|
+
addBodyParser(contentType, parser, opts = {}) {
|
|
102
|
+
this.bodyParsers[contentType] = {
|
|
103
|
+
parse: parser,
|
|
104
|
+
limit: parseLimit(opts.limit || this.opts.bodyLimit || "10m"),
|
|
105
|
+
};
|
|
66
106
|
return this;
|
|
67
107
|
}
|
|
68
108
|
listen(port, callback, beforeHandleRequest) {
|
package/dist/req.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { URL } from "url";
|
|
3
|
-
import {
|
|
3
|
+
import { getContentType, getRawBody } from "./body-utils.js";
|
|
4
|
+
import { getIP, parseCookies } from "./helpers.js";
|
|
4
5
|
import { getMiddlewares, matchMiddleware } from "./middleware.js";
|
|
5
6
|
import { FFResponse } from "./res.js";
|
|
6
7
|
import { validate } from "./valid.js";
|
|
@@ -18,23 +19,31 @@ export function handleRequest(req, res, FF, reRoute = false) {
|
|
|
18
19
|
const { logger } = FF;
|
|
19
20
|
try {
|
|
20
21
|
const [path, params] = (req.url || "").split("?");
|
|
21
|
-
const normalizedPath = path.replace(/\/{2,}/g, "/");
|
|
22
|
+
const normalizedPath = decodeURIComponent(path).replace(/\/{2,}/g, "/");
|
|
22
23
|
const parsedUrl = new URL(normalizedPath + (params ? `?${params}` : ""), "http://localhost");
|
|
23
|
-
req.path =
|
|
24
|
+
req.path = parsedUrl.pathname || "/";
|
|
24
25
|
req.query = Object.fromEntries(parsedUrl.searchParams);
|
|
26
|
+
req.cookies = parseCookies(req.headers.cookie || "");
|
|
25
27
|
}
|
|
26
28
|
catch (e) {
|
|
27
|
-
logger.error(`Error parsing
|
|
29
|
+
logger.error(`Error parsing request (${req.url}): ${e}`);
|
|
28
30
|
res.status(400).end("400: Bad request");
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
|
-
req.
|
|
33
|
+
req.ip = getIP(req);
|
|
32
34
|
req.params = {};
|
|
33
|
-
req.valid = (schema) => validate(
|
|
35
|
+
req.valid = (schema, regexRules) => validate({
|
|
36
|
+
schema,
|
|
37
|
+
data: req.body,
|
|
38
|
+
regexRules,
|
|
39
|
+
isBodyParsed: req.isBodyParsed,
|
|
40
|
+
});
|
|
34
41
|
req.reRoute = fn => {
|
|
35
42
|
fn(req);
|
|
36
43
|
handleRequest(req, res, FF, true);
|
|
37
44
|
};
|
|
45
|
+
req.header = (name) => req.headers[name.toLowerCase()] || "";
|
|
46
|
+
req.isBodyParsed = false;
|
|
38
47
|
if (FF.opts.xRequestId !== "disable") {
|
|
39
48
|
if (req.headers["x-request-id"]) {
|
|
40
49
|
req.id = req.headers["x-request-id"];
|
|
@@ -122,15 +131,30 @@ export function handleRequest(req, res, FF, reRoute = false) {
|
|
|
122
131
|
return;
|
|
123
132
|
}
|
|
124
133
|
req.body = {};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
logger.debug(`Executing body parser ${bodyParserIndex} of ${FF.bodyParsers.length}`);
|
|
132
|
-
const bodyParser = FF.bodyParsers[bodyParserIndex++];
|
|
133
|
-
bodyParser(req, res, nextBodyParser);
|
|
134
|
+
const ct = getContentType(req) || "application/json";
|
|
135
|
+
const entry = FF.bodyParsers[ct];
|
|
136
|
+
if (!entry) {
|
|
137
|
+
logger.debug("No body parser found for content-type. Executing middlewares");
|
|
138
|
+
next();
|
|
139
|
+
return;
|
|
134
140
|
}
|
|
135
|
-
|
|
141
|
+
logger.debug(`Executing body parser for content-type: ${ct}`);
|
|
142
|
+
getRawBody(req, res, entry.limit)
|
|
143
|
+
.then(async (rawBody) => {
|
|
144
|
+
try {
|
|
145
|
+
req.body = (await entry.parse(rawBody, req, res)) ?? {};
|
|
146
|
+
req.isBodyParsed = true;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!res._ended)
|
|
150
|
+
req.body = {};
|
|
151
|
+
}
|
|
152
|
+
next();
|
|
153
|
+
})
|
|
154
|
+
.catch(() => {
|
|
155
|
+
if (!res._ended) {
|
|
156
|
+
req.body = {};
|
|
157
|
+
next();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
136
160
|
}
|
package/dist/res.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare class FFResponse extends http.ServerResponse {
|
|
|
31
31
|
* @param options The options for the cookie
|
|
32
32
|
*/
|
|
33
33
|
cookie(name: string, value: string, options?: CookieOptions): this;
|
|
34
|
+
clearCookie(name: string, options?: CookieOptions): this;
|
|
34
35
|
/**
|
|
35
36
|
* Set the status code for the response
|
|
36
37
|
* @param code The status code to set
|
package/dist/res.js
CHANGED
|
@@ -44,7 +44,7 @@ export class FFResponse extends http.ServerResponse {
|
|
|
44
44
|
*/
|
|
45
45
|
cookie(name, value, options = {}) {
|
|
46
46
|
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
47
|
-
if (options.maxAge)
|
|
47
|
+
if (options.maxAge !== undefined)
|
|
48
48
|
cookie += `; Max-Age=${options.maxAge}`;
|
|
49
49
|
if (options.path)
|
|
50
50
|
cookie += `; Path=${options.path}`;
|
|
@@ -54,9 +54,15 @@ export class FFResponse extends http.ServerResponse {
|
|
|
54
54
|
cookie += `; Secure`;
|
|
55
55
|
if (options.sameSite)
|
|
56
56
|
cookie += `; SameSite=${options.sameSite}`;
|
|
57
|
-
this.
|
|
57
|
+
this.appendHeader("Set-Cookie", cookie);
|
|
58
58
|
return this;
|
|
59
59
|
}
|
|
60
|
+
clearCookie(name, options = {}) {
|
|
61
|
+
return this.cookie(name, "", {
|
|
62
|
+
...options,
|
|
63
|
+
maxAge: 0,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
60
66
|
/**
|
|
61
67
|
* Set the status code for the response
|
|
62
68
|
* @param code The status code to set
|
package/dist/router.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -17,7 +17,11 @@ export interface Query {
|
|
|
17
17
|
export interface Body {
|
|
18
18
|
[key: string]: any;
|
|
19
19
|
}
|
|
20
|
-
export type ParseBodyFunction = (body: string, req: FFRequest, res: FFResponse) => Promise<Record<string, any>>;
|
|
20
|
+
export type ParseBodyFunction = (body: string, req: FFRequest, res: FFResponse) => Record<string, any> | Promise<Record<string, any>>;
|
|
21
|
+
export interface BodyParserEntry {
|
|
22
|
+
parse: ParseBodyFunction;
|
|
23
|
+
limit: number;
|
|
24
|
+
}
|
|
21
25
|
export interface StandardBodyParserOptions {
|
|
22
26
|
limit?: string | number;
|
|
23
27
|
}
|
|
@@ -28,11 +32,14 @@ export declare class FFRequest extends http.IncomingMessage {
|
|
|
28
32
|
params: Params;
|
|
29
33
|
cookies: Cookies;
|
|
30
34
|
body: Body;
|
|
31
|
-
|
|
35
|
+
ip: string;
|
|
36
|
+
header: (name: string) => string;
|
|
37
|
+
valid: (schema: ValidationSchema, regexRules?: Record<string, RegExp | string>) => ValidationResult;
|
|
32
38
|
middleware: Middleware;
|
|
33
39
|
id?: string;
|
|
34
40
|
sseId?: string;
|
|
35
41
|
reRoute: (fn: (req: FFRequest) => void) => void;
|
|
42
|
+
isBodyParsed: boolean;
|
|
36
43
|
_compression?: true;
|
|
37
44
|
}
|
|
38
45
|
export interface Middleware {
|
|
@@ -81,8 +88,15 @@ export interface RenderOptions {
|
|
|
81
88
|
export interface FFOpts {
|
|
82
89
|
loggerOpts?: LoggerOptions;
|
|
83
90
|
bodyLimit?: string;
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
disableParser?: {
|
|
92
|
+
json?: boolean;
|
|
93
|
+
urlencoded?: boolean;
|
|
94
|
+
json5?: boolean;
|
|
95
|
+
yaml?: boolean;
|
|
96
|
+
toml?: boolean;
|
|
97
|
+
xml?: boolean;
|
|
98
|
+
text?: boolean;
|
|
99
|
+
};
|
|
86
100
|
xRequestId?: "auto" | "disable" | "manual";
|
|
87
101
|
}
|
|
88
102
|
export type FFVars = {
|
package/dist/types.js
CHANGED
package/dist/valid.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { RouteHandler, ValidationResult, ValidationSchema } from "./types.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
2
|
+
export interface ValidateBody {
|
|
3
|
+
schema: ValidationSchema;
|
|
4
|
+
data: any;
|
|
5
|
+
regexRules: Record<string, RegExp | string>;
|
|
6
|
+
isBodyParsed: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function validate(opts: ValidateBody): ValidationResult;
|
|
9
|
+
export declare function validateBody(schema: ValidationSchema, regexRules?: Record<string, RegExp | string>): RouteHandler;
|
package/dist/valid.js
CHANGED
|
@@ -1,28 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
const optionalKeys = new Set([
|
|
2
|
+
"nullable",
|
|
3
|
+
"optional",
|
|
4
|
+
"opt",
|
|
5
|
+
]);
|
|
6
|
+
export function validate(opts) {
|
|
7
|
+
if (!opts.isBodyParsed) {
|
|
8
|
+
return {
|
|
9
|
+
valid: false,
|
|
10
|
+
validErrors: {
|
|
11
|
+
_body: [
|
|
12
|
+
"Request body could not be parsed. Check Content-Type header and body format.",
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const { schema, data, regexRules } = opts;
|
|
2
18
|
const errors = {};
|
|
3
19
|
let isValid = true;
|
|
4
20
|
for (const key in schema) {
|
|
5
21
|
const rules = schema[key].split("|");
|
|
6
22
|
const value = data[key];
|
|
7
23
|
const fieldErrors = [];
|
|
24
|
+
const isOptional = rules.some(r => optionalKeys.has(r));
|
|
25
|
+
if (isOptional && (value === null || value === undefined)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
8
28
|
for (const rule of rules) {
|
|
9
29
|
const [ruleName, param] = rule.split(":");
|
|
10
30
|
switch (ruleName) {
|
|
11
31
|
case "required":
|
|
32
|
+
case "r":
|
|
12
33
|
if (!value && value !== 0) {
|
|
13
34
|
fieldErrors.push(`${key} is required`);
|
|
14
35
|
}
|
|
15
36
|
break;
|
|
16
37
|
case "string":
|
|
38
|
+
case "str":
|
|
39
|
+
case "s":
|
|
17
40
|
if (typeof value !== "string") {
|
|
18
41
|
fieldErrors.push(`${key} must be a string`);
|
|
19
42
|
}
|
|
20
43
|
break;
|
|
21
44
|
case "number":
|
|
45
|
+
case "num":
|
|
46
|
+
case "n":
|
|
22
47
|
if (typeof value !== "number") {
|
|
23
48
|
fieldErrors.push(`${key} must be a number`);
|
|
24
49
|
}
|
|
25
50
|
break;
|
|
51
|
+
case "integer":
|
|
52
|
+
case "int":
|
|
53
|
+
case "i":
|
|
54
|
+
if (!Number.isInteger(value)) {
|
|
55
|
+
fieldErrors.push(`${key} must be an integer`);
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
case "boolean":
|
|
59
|
+
case "bool":
|
|
60
|
+
case "b":
|
|
61
|
+
if (typeof value !== "boolean") {
|
|
62
|
+
fieldErrors.push(`${key} must be a boolean`);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case "array":
|
|
66
|
+
case "arr":
|
|
67
|
+
case "a":
|
|
68
|
+
if (!Array.isArray(value)) {
|
|
69
|
+
fieldErrors.push(`${key} must be an array`);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "object":
|
|
73
|
+
case "obj":
|
|
74
|
+
case "o":
|
|
75
|
+
if (typeof value !== "object" ||
|
|
76
|
+
Array.isArray(value) ||
|
|
77
|
+
value === null) {
|
|
78
|
+
fieldErrors.push(`${key} must be an object`);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
26
81
|
case "min":
|
|
27
82
|
if (typeof value === "string" && value.length < parseInt(param)) {
|
|
28
83
|
fieldErrors.push(`${key} must be at least ${param} characters long`);
|
|
@@ -30,6 +85,9 @@ export function validate(schema, data) {
|
|
|
30
85
|
if (typeof value === "number" && value < parseInt(param)) {
|
|
31
86
|
fieldErrors.push(`${key} must be at least ${param}`);
|
|
32
87
|
}
|
|
88
|
+
if (Array.isArray(value) && value.length < parseInt(param)) {
|
|
89
|
+
fieldErrors.push(`${key} must have at least ${param} items`);
|
|
90
|
+
}
|
|
33
91
|
break;
|
|
34
92
|
case "max":
|
|
35
93
|
if (typeof value === "string" && value.length > parseInt(param)) {
|
|
@@ -38,7 +96,36 @@ export function validate(schema, data) {
|
|
|
38
96
|
if (typeof value === "number" && value > parseInt(param)) {
|
|
39
97
|
fieldErrors.push(`${key} must not exceed ${param}`);
|
|
40
98
|
}
|
|
99
|
+
if (Array.isArray(value) && value.length > parseInt(param)) {
|
|
100
|
+
fieldErrors.push(`${key} must not exceed ${param} items`);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
case "between": {
|
|
104
|
+
const [min, max] = param.split(",");
|
|
105
|
+
if (typeof value === "number" &&
|
|
106
|
+
(value < parseInt(min) || value > parseInt(max))) {
|
|
107
|
+
fieldErrors.push(`${key} must be between ${min} and ${max}`);
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === "string" &&
|
|
110
|
+
(value.length < parseInt(min) || value.length > parseInt(max))) {
|
|
111
|
+
fieldErrors.push(`${key} must be between ${min} and ${max} characters`);
|
|
112
|
+
}
|
|
41
113
|
break;
|
|
114
|
+
}
|
|
115
|
+
case "in": {
|
|
116
|
+
const values = param.split(",");
|
|
117
|
+
if (!values.includes(String(value))) {
|
|
118
|
+
fieldErrors.push(`${key} must be one of: ${values.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "not_in": {
|
|
123
|
+
const values = param.split(",");
|
|
124
|
+
if (values.includes(String(value))) {
|
|
125
|
+
fieldErrors.push(`${key} must not be one of: ${values.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
42
129
|
case "email": {
|
|
43
130
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
44
131
|
if (!emailRegex.test(value)) {
|
|
@@ -46,6 +133,29 @@ export function validate(schema, data) {
|
|
|
46
133
|
}
|
|
47
134
|
break;
|
|
48
135
|
}
|
|
136
|
+
case "same": {
|
|
137
|
+
const otherValue = data[param];
|
|
138
|
+
if (value !== otherValue) {
|
|
139
|
+
fieldErrors.push(`${key} must be the same as ${param}`);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "diff":
|
|
144
|
+
case "different": {
|
|
145
|
+
const otherValue = data[param];
|
|
146
|
+
if (value === otherValue) {
|
|
147
|
+
fieldErrors.push(`${key} must be different from ${param}`);
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (regexRules && regexRules[key] !== undefined) {
|
|
154
|
+
const regex = regexRules[key] instanceof RegExp
|
|
155
|
+
? regexRules[key]
|
|
156
|
+
: new RegExp(regexRules[key]);
|
|
157
|
+
if (!regex.test(value)) {
|
|
158
|
+
fieldErrors.push(`${key} format is invalid`);
|
|
49
159
|
}
|
|
50
160
|
}
|
|
51
161
|
if (fieldErrors.length > 0) {
|
|
@@ -58,13 +168,14 @@ export function validate(schema, data) {
|
|
|
58
168
|
validErrors: errors,
|
|
59
169
|
};
|
|
60
170
|
}
|
|
61
|
-
export function validateBody(schema) {
|
|
171
|
+
export function validateBody(schema, regexRules) {
|
|
62
172
|
return (req, res, next) => {
|
|
63
|
-
const validationResult = req.valid(schema);
|
|
64
|
-
if (validationResult.valid) {
|
|
65
|
-
|
|
173
|
+
const validationResult = req.valid(schema, regexRules);
|
|
174
|
+
if (!validationResult.valid) {
|
|
175
|
+
const errorResponse = res.FF._400_formatter(validationResult.validErrors);
|
|
176
|
+
res.status(400).json(errorResponse);
|
|
177
|
+
return;
|
|
66
178
|
}
|
|
67
|
-
|
|
68
|
-
res.status(400).json(errorResponse);
|
|
179
|
+
next();
|
|
69
180
|
};
|
|
70
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wxn0brp/falcon-frame",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "wxn0brP",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"middleware-chain"
|
|
24
24
|
],
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/
|
|
26
|
+
"@types/bun": "*",
|
|
27
27
|
"@wxn0brp/biome": "*",
|
|
28
28
|
"tsc-alias": "*",
|
|
29
29
|
"typescript": "^7"
|