mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor: Rewrite functions into packages
This commit is contained in:
parent
847e679a10
commit
78f216092b
21 changed files with 1426 additions and 70 deletions
170
packages/request-parser/index.ts
Normal file
170
packages/request-parser/index.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* RequestParser
|
||||
* @file index.ts
|
||||
* @module request-parser
|
||||
* @description Parses Request object into a JavaScript object based on the content type
|
||||
*/
|
||||
|
||||
/**
|
||||
* RequestParser
|
||||
* Parses Request object into a JavaScript object
|
||||
* based on the Content-Type header
|
||||
* @param request Request object
|
||||
* @returns JavaScript object of type T
|
||||
*/
|
||||
export class RequestParser {
|
||||
constructor(public request: Request) {}
|
||||
|
||||
/**
|
||||
* Parse request body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
async toObject<T>() {
|
||||
try {
|
||||
switch (await this.determineContentType()) {
|
||||
case "application/json":
|
||||
return this.parseJson<T>();
|
||||
case "application/x-www-form-urlencoded":
|
||||
return this.parseFormUrlencoded<T>();
|
||||
case "multipart/form-data":
|
||||
return this.parseFormData<T>();
|
||||
default:
|
||||
return this.parseQuery<T>();
|
||||
}
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine body content type
|
||||
* If there is no Content-Type header, automatically
|
||||
* guess content type. Cuts off after ";" character
|
||||
* @returns Content-Type header value, or empty string if there is no body
|
||||
* @throws Error if body is invalid
|
||||
* @private
|
||||
*/
|
||||
private async determineContentType() {
|
||||
if (this.request.headers.get("Content-Type")) {
|
||||
return (
|
||||
this.request.headers.get("Content-Type")?.split(";")[0] ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
// Check if body is valid JSON
|
||||
try {
|
||||
await this.request.json();
|
||||
return "application/json";
|
||||
} catch {
|
||||
// This is not JSON
|
||||
}
|
||||
|
||||
// Check if body is valid FormData
|
||||
try {
|
||||
await this.request.formData();
|
||||
return "multipart/form-data";
|
||||
} catch {
|
||||
// This is not FormData
|
||||
}
|
||||
|
||||
if (this.request.body) {
|
||||
throw new Error("Invalid body");
|
||||
}
|
||||
|
||||
// If there is no body, return query parameters
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse FormData body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormData<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
result[key as keyof T] = value as any;
|
||||
} else if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
|
||||
(result[arrayKey] as any[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
|
||||
(result[arrayKey] as any[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseJson<T>(): Promise<Partial<T>> {
|
||||
try {
|
||||
return (await this.request.json()) as T;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse query parameters into a JavaScript object
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
* @returns JavaScript object of type T
|
||||
*/
|
||||
private parseQuery<T>(): Partial<T> {
|
||||
const result: Partial<T> = {};
|
||||
const url = new URL(this.request.url);
|
||||
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
(result[arrayKey] as string[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
6
packages/request-parser/package.json
Normal file
6
packages/request-parser/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "request-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
158
packages/request-parser/tests/request-parser.test.ts
Normal file
158
packages/request-parser/tests/request-parser.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { describe, it, expect, test } from "bun:test";
|
||||
import { RequestParser } from "..";
|
||||
|
||||
describe("RequestParser", () => {
|
||||
describe("Should parse query parameters correctly", () => {
|
||||
test("With text parameters", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2"
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
|
||||
test("With Array", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?test[]=value1&test[]=value2"
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result.test).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
test("With both at once", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2"
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result).toEqual({
|
||||
param1: "value1",
|
||||
param2: "value2",
|
||||
test: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse JSON body correctly", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ param1: "value1", param2: "value2" }),
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
|
||||
it("should handle invalid JSON body", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "invalid json",
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
describe("should parse form data correctly", () => {
|
||||
test("With basic text parameters", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("param1", "value1");
|
||||
formData.append("param2", "value2");
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
|
||||
test("With File object", async () => {
|
||||
const file = new File(["content"], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
file: File;
|
||||
}>();
|
||||
expect(result.file).toBeInstanceOf(File);
|
||||
expect(await result.file?.text()).toEqual("content");
|
||||
});
|
||||
|
||||
test("With Array", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("test[]", "value1");
|
||||
formData.append("test[]", "value2");
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result.test).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
test("With all three at once", async () => {
|
||||
const file = new File(["content"], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("param1", "value1");
|
||||
formData.append("param2", "value2");
|
||||
formData.append("file", file);
|
||||
formData.append("test[]", "value1");
|
||||
formData.append("test[]", "value2");
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
file: File;
|
||||
test: string[];
|
||||
}>();
|
||||
expect(result).toEqual({
|
||||
param1: "value1",
|
||||
param2: "value2",
|
||||
file: file,
|
||||
test: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
|
||||
test("URL Encoded", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "param1=value1¶m2=value2",
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue