mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
feat(api): ✨ Implement filters API v2 (with some routes missing)
This commit is contained in:
parent
ce082f8e6a
commit
a37e8e92c5
21 changed files with 3087 additions and 154 deletions
|
|
@ -1,9 +1,4 @@
|
|||
/**
|
||||
* RequestParser
|
||||
* @file index.ts
|
||||
* @module request-parser
|
||||
* @description Parses Request object into a JavaScript object based on the content type
|
||||
*/
|
||||
import { parse } from "qs";
|
||||
|
||||
/**
|
||||
* RequestParser
|
||||
|
|
@ -98,19 +93,38 @@ export class RequestParser {
|
|||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof Blob) {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
} else if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
// Check if there are any files in the FormData
|
||||
if (
|
||||
Array.from(formData.values()).some((value) => value instanceof Blob)
|
||||
) {
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof Blob) {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
} 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 FormDataEntryValue[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Convert to URLSearchParams and parse as query
|
||||
const searchParams = new URLSearchParams([
|
||||
...formData.entries(),
|
||||
] as [string, string][]);
|
||||
|
||||
const parsed = parse(searchParams.toString(), {
|
||||
parseArrays: true,
|
||||
interpretNumericEntities: true,
|
||||
});
|
||||
|
||||
return castBooleanObject(
|
||||
parsed as PossiblyRecursiveObject,
|
||||
) as Partial<T>;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -159,29 +173,49 @@ export class RequestParser {
|
|||
* @returns JavaScript object of type T
|
||||
*/
|
||||
private parseQuery<T>(): Partial<T> {
|
||||
const result: Partial<T> = {};
|
||||
const url = new URL(this.request.url);
|
||||
const parsed = parse(
|
||||
new URL(this.request.url).searchParams.toString(),
|
||||
{
|
||||
parseArrays: true,
|
||||
interpretNumericEntities: true,
|
||||
},
|
||||
);
|
||||
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
if (decodeURIComponent(key).endsWith("[]")) {
|
||||
const arrayKey = decodeURIComponent(key).slice(
|
||||
0,
|
||||
-2,
|
||||
) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
(result[arrayKey] as string[]).push(decodeURIComponent(value));
|
||||
} else {
|
||||
result[key as keyof T] = castBoolean(
|
||||
decodeURIComponent(value),
|
||||
) as T[keyof T];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return castBooleanObject(
|
||||
parsed as PossiblyRecursiveObject,
|
||||
) as Partial<T>;
|
||||
}
|
||||
}
|
||||
|
||||
interface PossiblyRecursiveObject {
|
||||
[key: string]:
|
||||
| PossiblyRecursiveObject[]
|
||||
| PossiblyRecursiveObject
|
||||
| string
|
||||
| string[]
|
||||
| boolean;
|
||||
}
|
||||
|
||||
// Recursive
|
||||
const castBooleanObject = (value: PossiblyRecursiveObject | string) => {
|
||||
if (typeof value === "string") {
|
||||
return castBoolean(value);
|
||||
}
|
||||
|
||||
for (const key in value) {
|
||||
const child = value[key];
|
||||
if (Array.isArray(child)) {
|
||||
value[key] = child.map((v) => castBooleanObject(v)) as string[];
|
||||
} else if (typeof child === "object") {
|
||||
value[key] = castBooleanObject(child);
|
||||
} else {
|
||||
value[key] = castBoolean(child as string);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const castBoolean = (value: string) => {
|
||||
if (["true"].includes(value)) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"name": "request-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
"name": "request-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "qs": "^6.12.1" },
|
||||
"devDependencies": {
|
||||
"@types/qs": "^6.9.15"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,28 @@ describe("RequestParser", () => {
|
|||
expect(result.test).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
test("With Array of objects", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?test[][key]=value1&test[][value]=value2",
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: { key: string; value: string }[];
|
||||
}>();
|
||||
expect(result.test).toEqual([{ key: "value1", value: "value2" }]);
|
||||
});
|
||||
|
||||
test("With Array of multiple objects", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?test[][key]=value1&test[][value]=value2&test[][key]=value3&test[][value]=value4",
|
||||
);
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
test: { key: string[]; value: string[] }[];
|
||||
}>();
|
||||
expect(result.test).toEqual([
|
||||
{ key: ["value1", "value3"], value: ["value2", "value4"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("With both at once", async () => {
|
||||
const request = new Request(
|
||||
"http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2",
|
||||
|
|
|
|||
|
|
@ -86,14 +86,12 @@ export const processRoute = async (
|
|||
return errorResponse("Method not allowed", 405);
|
||||
}
|
||||
|
||||
let auth: AuthData | null = null;
|
||||
const auth: AuthData = await getFromRequest(request);
|
||||
|
||||
if (
|
||||
route.meta.auth.required ||
|
||||
route.meta.auth.requiredOnMethods?.includes(request.method as HttpVerb)
|
||||
) {
|
||||
auth = await getFromRequest(request);
|
||||
|
||||
if (!auth.user) {
|
||||
return errorResponse(
|
||||
"Unauthorized: access to this method requires an authenticated user",
|
||||
|
|
@ -112,7 +110,7 @@ export const processRoute = async (
|
|||
}
|
||||
}
|
||||
|
||||
const parsedRequest = await new RequestParser(request)
|
||||
const parsedRequest = await new RequestParser(request.clone())
|
||||
.toObject()
|
||||
.catch(async (err) => {
|
||||
await logger.logError(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue