refactor(api): 🎨 Refactor complex functions into smaller ones

This commit is contained in:
Jesse Wierzbinski 2024-06-12 18:16:59 -10:00
parent a1e02d0d78
commit c61f519a34
No known key found for this signature in database
5 changed files with 277 additions and 412 deletions

View file

@ -2,7 +2,7 @@ import { idValidator } from "@/api";
import { dualLogger } from "@/loggers"; import { dualLogger } from "@/loggers";
import { proxyUrl } from "@/response"; import { proxyUrl } from "@/response";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import type { EntityValidator } from "@lysand-org/federation"; import { EntityValidator } from "@lysand-org/federation";
import { import {
type InferInsertModel, type InferInsertModel,
type SQL, type SQL,
@ -33,6 +33,7 @@ import {
type StatusWithRelations, type StatusWithRelations,
contentToHtml, contentToHtml,
findManyNotes, findManyNotes,
parseTextMentions,
} from "~/database/entities/status"; } from "~/database/entities/status";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { import {
@ -249,90 +250,79 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
return this; return this;
} }
static async fromData( static async fromData(data: {
author: User, author: User;
content: typeof EntityValidator.$ContentFormat, content: typeof EntityValidator.$ContentFormat;
visibility: apiStatus["visibility"], visibility: apiStatus["visibility"];
isSensitive: boolean, isSensitive: boolean;
spoilerText: string, spoilerText: string;
emojis: EmojiWithInstance[], emojis?: EmojiWithInstance[];
uri?: string, uri?: string;
mentions?: User[], mentions?: User[];
/** List of IDs of database Attachment objects */ /** List of IDs of database Attachment objects */
mediaAttachments?: string[], mediaAttachments?: string[];
replyId?: string, replyId?: string;
quoteId?: string, quoteId?: string;
application?: Application, application?: Application;
): Promise<Note | null> { }): Promise<Note> {
const htmlContent = await contentToHtml(content, mentions); const plaintextContent =
data.content["text/plain"]?.content ??
Object.entries(data.content)[0][1].content;
// Parse emojis and fuse with existing emojis const parsedMentions = [
let foundEmojis = emojis; ...(data.mentions ?? []),
...(await parseTextMentions(plaintextContent)),
// Deduplicate by .id
].filter(
(mention, index, self) =>
index === self.findIndex((t) => t.id === mention.id),
);
if (author.isLocal()) { const parsedEmojis = [
const parsedEmojis = await parseEmojis(htmlContent); ...(data.emojis ?? []),
// Fuse and deduplicate ...(await parseEmojis(plaintextContent)),
foundEmojis = [...emojis, ...parsedEmojis].filter( // Deduplicate by .id
(emoji, index, self) => ].filter(
index === self.findIndex((t) => t.id === emoji.id), (emoji, index, self) =>
); index === self.findIndex((t) => t.id === emoji.id),
} );
const htmlContent = await contentToHtml(data.content, parsedMentions);
const newNote = await Note.insert({ const newNote = await Note.insert({
authorId: author.id, authorId: data.author.id,
content: htmlContent, content: htmlContent,
contentSource: contentSource:
content["text/plain"]?.content || data.content["text/plain"]?.content ||
content["text/markdown"]?.content || data.content["text/markdown"]?.content ||
Object.entries(content)[0][1].content || Object.entries(data.content)[0][1].content ||
"", "",
contentType: "text/html", contentType: "text/html",
visibility, visibility: data.visibility,
sensitive: isSensitive, sensitive: data.isSensitive,
spoilerText: await sanitizedHtmlStrip(spoilerText), spoilerText: await sanitizedHtmlStrip(data.spoilerText),
uri: uri || null, uri: data.uri || null,
replyId: replyId ?? null, replyId: data.replyId ?? null,
quotingId: quoteId ?? null, quotingId: data.quoteId ?? null,
applicationId: application?.id ?? null, applicationId: data.application?.id ?? null,
}); });
// Connect emojis // Connect emojis
for (const emoji of foundEmojis) { await newNote.recalculateDatabaseEmojis(parsedEmojis);
await db
.insert(EmojiToNote)
.values({
emojiId: emoji.id,
noteId: newNote.id,
})
.execute();
}
// Connect mentions // Connect mentions
for (const mention of mentions ?? []) { await newNote.recalculateDatabaseMentions(parsedMentions);
await db
.insert(NoteToMentions)
.values({
noteId: newNote.id,
userId: mention.id,
})
.execute();
}
// Set attachment parents // Set attachment parents
if (mediaAttachments && mediaAttachments.length > 0) { await newNote.recalculateDatabaseAttachments(
await db data.mediaAttachments ?? [],
.update(Attachments) );
.set({
noteId: newNote.id,
})
.where(inArray(Attachments.id, mediaAttachments));
}
// Send notifications for mentioned local users // Send notifications for mentioned local users
for (const mention of mentions ?? []) { for (const mention of parsedMentions ?? []) {
if (mention.isLocal()) { if (mention.isLocal()) {
await db.insert(Notifications).values({ await db.insert(Notifications).values({
accountId: author.id, accountId: data.author.id,
notifiedId: mention.id, notifiedId: mention.id,
type: "mention", type: "mention",
noteId: newNote.id, noteId: newNote.id,
@ -340,61 +330,101 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
} }
} }
return await Note.fromId(newNote.id, newNote.data.authorId); await newNote.reload();
return newNote;
} }
async updateFromData( async updateFromData(data: {
content?: typeof EntityValidator.$ContentFormat, author?: User;
visibility?: apiStatus["visibility"], content?: typeof EntityValidator.$ContentFormat;
isSensitive?: boolean, visibility?: apiStatus["visibility"];
spoilerText?: string, isSensitive?: boolean;
emojis: EmojiWithInstance[] = [], spoilerText?: string;
mentions: User[] = [], emojis?: EmojiWithInstance[];
uri?: string;
mentions?: User[];
/** List of IDs of database Attachment objects */ /** List of IDs of database Attachment objects */
mediaAttachments: string[] = [], mediaAttachments?: string[];
replyId?: string, replyId?: string;
quoteId?: string, quoteId?: string;
application?: Application, application?: Application;
) { }): Promise<Note> {
const htmlContent = content const plaintextContent = data.content
? await contentToHtml(content, mentions) ? data.content["text/plain"]?.content ??
Object.entries(data.content)[0][1].content
: undefined; : undefined;
// Parse emojis and fuse with existing emojis const parsedMentions = [
let foundEmojis = emojis; ...(data.mentions ?? []),
...(plaintextContent
? await parseTextMentions(plaintextContent)
: []),
// Deduplicate by .id
].filter(
(mention, index, self) =>
index === self.findIndex((t) => t.id === mention.id),
);
if (this.author.isLocal() && htmlContent) { const parsedEmojis = [
const parsedEmojis = await parseEmojis(htmlContent); ...(data.emojis ?? []),
// Fuse and deduplicate ...(plaintextContent ? await parseEmojis(plaintextContent) : []),
foundEmojis = [...emojis, ...parsedEmojis].filter( // Deduplicate by .id
(emoji, index, self) => ].filter(
index === self.findIndex((t) => t.id === emoji.id), (emoji, index, self) =>
); index === self.findIndex((t) => t.id === emoji.id),
} );
const newNote = await this.update({ const htmlContent = data.content
? await contentToHtml(data.content, parsedMentions)
: undefined;
await this.update({
content: htmlContent, content: htmlContent,
contentSource: content contentSource: data.content
? content["text/plain"]?.content || ? data.content["text/plain"]?.content ||
content["text/markdown"]?.content || data.content["text/markdown"]?.content ||
Object.entries(content)[0][1].content || Object.entries(data.content)[0][1].content ||
"" ""
: undefined, : undefined,
contentType: "text/html", contentType: "text/html",
visibility, visibility: data.visibility,
sensitive: isSensitive, sensitive: data.isSensitive,
spoilerText: spoilerText, spoilerText: data.spoilerText,
replyId, replyId: data.replyId,
quotingId: quoteId, quotingId: data.quoteId,
applicationId: application?.id, applicationId: data.application?.id,
}); });
// Connect emojis
await this.recalculateDatabaseEmojis(parsedEmojis);
// Connect mentions
await this.recalculateDatabaseMentions(parsedMentions);
// Set attachment parents
await this.recalculateDatabaseAttachments(data.mediaAttachments ?? []);
await this.reload();
return this;
}
public async recalculateDatabaseEmojis(
emojis: EmojiWithInstance[],
): Promise<void> {
// Fuse and deduplicate
const fusedEmojis = emojis.filter(
(emoji, index, self) =>
index === self.findIndex((t) => t.id === emoji.id),
);
// Connect emojis // Connect emojis
await db await db
.delete(EmojiToNote) .delete(EmojiToNote)
.where(eq(EmojiToNote.noteId, this.data.id)); .where(eq(EmojiToNote.noteId, this.data.id));
for (const emoji of foundEmojis) { for (const emoji of fusedEmojis) {
await db await db
.insert(EmojiToNote) .insert(EmojiToNote)
.values({ .values({
@ -403,13 +433,15 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
}) })
.execute(); .execute();
} }
}
public async recalculateDatabaseMentions(mentions: User[]): Promise<void> {
// Connect mentions // Connect mentions
await db await db
.delete(NoteToMentions) .delete(NoteToMentions)
.where(eq(NoteToMentions.noteId, this.data.id)); .where(eq(NoteToMentions.noteId, this.data.id));
for (const mention of mentions ?? []) { for (const mention of mentions) {
await db await db
.insert(NoteToMentions) .insert(NoteToMentions)
.values({ .values({
@ -418,27 +450,27 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
}) })
.execute(); .execute();
} }
}
public async recalculateDatabaseAttachments(
mediaAttachments: string[],
): Promise<void> {
// Set attachment parents // Set attachment parents
if (mediaAttachments) { await db
.update(Attachments)
.set({
noteId: null,
})
.where(eq(Attachments.noteId, this.data.id));
if (mediaAttachments.length > 0) {
await db await db
.update(Attachments) .update(Attachments)
.set({ .set({
noteId: null, noteId: this.data.id,
}) })
.where(eq(Attachments.noteId, this.data.id)); .where(inArray(Attachments.id, mediaAttachments));
if (mediaAttachments.length > 0) {
await db
.update(Attachments)
.set({
noteId: this.data.id,
})
.where(inArray(Attachments.id, mediaAttachments));
}
} }
return await Note.fromId(newNote.id, newNote.authorId);
} }
static async resolve( static async resolve(
@ -476,10 +508,6 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
throw new Error("No URI or note provided"); throw new Error("No URI or note provided");
} }
const foundStatus = await Note.fromSql(
eq(Notes.uri, uri ?? providedNote?.uri ?? ""),
);
let note = providedNote || null; let note = providedNote || null;
if (uri) { if (uri) {
@ -494,27 +522,44 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
}, },
}); });
note = (await response.json()) as typeof EntityValidator.$Note; note = await new EntityValidator().Note(await response.json());
} }
if (!note) { if (!note) {
throw new Error("No note was able to be fetched"); throw new Error("No note was able to be fetched");
} }
if (note.type !== "Note") {
throw new Error("Invalid object type");
}
if (!note.author) {
throw new Error("Invalid object author");
}
const author = await User.resolve(note.author); const author = await User.resolve(note.author);
if (!author) { if (!author) {
throw new Error("Invalid object author"); throw new Error("Invalid object author");
} }
return await Note.fromLysand(note, author);
}
static async fromLysand(
note: typeof EntityValidator.$Note,
author: User,
): Promise<Note> {
const emojis = [];
for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
?.emojis ?? []) {
const resolvedEmoji = await fetchEmoji(emoji).catch((e) => {
dualLogger.logError(
LogLevel.Error,
"Federation.StatusResolver",
e,
);
return null;
});
if (resolvedEmoji) {
emojis.push(resolvedEmoji);
}
}
const attachments = []; const attachments = [];
for (const attachment of note.attachments ?? []) { for (const attachment of note.attachments ?? []) {
@ -534,85 +579,45 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
} }
} }
const emojis = []; const newData = {
for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
?.emojis ?? []) {
const resolvedEmoji = await fetchEmoji(emoji).catch((e) => {
dualLogger.logError(
LogLevel.Error,
"Federation.StatusResolver",
e,
);
return null;
});
if (resolvedEmoji) {
emojis.push(resolvedEmoji);
}
}
if (foundStatus) {
return await foundStatus.updateFromData(
note.content ?? {
"text/plain": {
content: "",
},
},
note.visibility as apiStatus["visibility"],
note.is_sensitive ?? false,
note.subject ?? "",
emojis,
note.mentions
? await Promise.all(
(note.mentions ?? [])
.map((mention) => User.resolve(mention))
.filter(
(mention) => mention !== null,
) as Promise<User>[],
)
: [],
attachments.map((a) => a.id),
note.replies_to
? (await Note.resolve(note.replies_to))?.data.id
: undefined,
note.quotes
? (await Note.resolve(note.quotes))?.data.id
: undefined,
);
}
const createdNote = await Note.fromData(
author, author,
note.content ?? { content: note.content ?? {
"text/plain": { "text/plain": {
content: "", content: "",
}, },
}, },
note.visibility as apiStatus["visibility"], visibility: note.visibility as apiStatus["visibility"],
note.is_sensitive ?? false, isSensitive: note.is_sensitive ?? false,
note.subject ?? "", spoilerText: note.subject ?? "",
emojis, emojis,
note.uri, uri: note.uri,
await Promise.all( mentions: await Promise.all(
(note.mentions ?? []) (note.mentions ?? [])
.map((mention) => User.resolve(mention)) .map((mention) => User.resolve(mention))
.filter((mention) => mention !== null) as Promise<User>[], .filter((mention) => mention !== null) as Promise<User>[],
), ),
attachments.map((a) => a.id), mediaAttachments: attachments.map((a) => a.id),
note.replies_to replyId: note.replies_to
? (await Note.resolve(note.replies_to))?.data.id ? (await Note.resolve(note.replies_to))?.data.id
: undefined, : undefined,
note.quotes quoteId: note.quotes
? (await Note.resolve(note.quotes))?.data.id ? (await Note.resolve(note.quotes))?.data.id
: undefined, : undefined,
); };
if (!createdNote) { // Check if new note already exists
throw new Error("Failed to create status");
const foundNote = await Note.fromSql(eq(Notes.uri, note.uri));
// If it exists, simply update it
if (foundNote) {
await foundNote.updateFromData(newData);
return foundNote;
} }
return createdNote; // Else, create a new note
return await Note.fromData(newData);
} }
async delete(ids: string[]): Promise<void>; async delete(ids: string[]): Promise<void>;

View file

@ -1,4 +1,4 @@
import { applyConfig, auth, handleZodError, qs } from "@/api"; import { applyConfig, auth, handleZodError, jsonOrForm } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import { sanitizedHtmlStrip } from "@/sanitization"; import { sanitizedHtmlStrip } from "@/sanitization";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
@ -99,7 +99,7 @@ export default (app: Hono) =>
app.on( app.on(
meta.allowedMethods, meta.allowedMethods,
meta.route, meta.route,
qs(), jsonOrForm(),
zValidator("form", schemas.form, handleZodError), zValidator("form", schemas.form, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {

View file

@ -159,21 +159,18 @@ export default (app: Hono) =>
} }
} }
const newNote = await foundStatus.updateFromData( const newNote = await foundStatus.updateFromData({
statusText content: statusText
? { ? {
[content_type]: { [content_type]: {
content: statusText, content: statusText,
}, },
} }
: undefined, : undefined,
undefined, isSensitive: sensitive,
sensitive, spoilerText: spoiler_text,
spoiler_text, mediaAttachments: media_ids,
undefined, });
undefined,
media_ids,
);
if (!newNote) { if (!newNote) {
return errorResponse("Failed to update status", 500); return errorResponse("Failed to update status", 500);

View file

@ -5,7 +5,7 @@ import { config } from "config-manager";
import type { Hono } from "hono"; import type { Hono } from "hono";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { federateNote, parseTextMentions } from "~/database/entities/status"; import { federateNote } from "~/database/entities/status";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
@ -175,26 +175,21 @@ export default (app: Hono) =>
} }
} }
const mentions = await parseTextMentions(status ?? ""); const newNote = await Note.fromData({
author: user,
const newNote = await Note.fromData( content: {
user,
{
[content_type]: { [content_type]: {
content: status ?? "", content: status ?? "",
}, },
}, },
visibility, visibility,
sensitive ?? false, isSensitive: sensitive ?? false,
spoiler_text ?? "", spoilerText: spoiler_text ?? "",
[], mediaAttachments: media_ids,
undefined, replyId: in_reply_to_id ?? undefined,
mentions, quoteId: quote_id ?? undefined,
media_ids, application: application ?? undefined,
in_reply_to_id ?? undefined, });
quote_id ?? undefined,
application ?? undefined,
);
if (!newNote) { if (!newNote) {
return errorResponse("Failed to create status", 500); return errorResponse("Failed to create status", 500);

View file

@ -3,7 +3,6 @@ import chalk from "chalk";
import { config } from "config-manager"; import { config } from "config-manager";
import type { Context } from "hono"; import type { Context } from "hono";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import type { BodyData } from "hono/utils/body";
import { validator } from "hono/validator"; import { validator } from "hono/validator";
import { import {
anyOf, anyOf,
@ -198,148 +197,45 @@ export const auth = (
return checkRouteNeedsAuth(auth, authData, context); return checkRouteNeedsAuth(auth, authData, context);
}); });
/* export const auth = ( // Helper function to parse form data
authData: ApiRouteMetadata["auth"], async function parseFormData(context: Context) {
permissionData?: ApiRouteMetadata["permissions"], const formData = await context.req.formData();
) => const urlparams = new URLSearchParams();
validator("header", async (value, context) => { const files = new Map<string, File>();
const auth = value.authorization for (const [key, value] of [...formData.entries()]) {
? await getFromHeader(value.authorization) if (Array.isArray(value)) {
: null; for (const val of value) {
urlparams.append(key, val);
const error = errorResponse("Unauthorized", 401);
// Permissions check
if (permissionData) {
const userPerms = auth?.user
? auth.user.getAllPermissions()
: config.permissions.anonymous;
const requiredPerms =
permissionData.methodOverrides?.[
context.req.method as HttpVerb
] ?? permissionData.required;
if (!requiredPerms.every((perm) => userPerms.includes(perm))) {
const missingPerms = requiredPerms.filter(
(perm) => !userPerms.includes(perm),
);
return context.json(
{
error: `You do not have the required permissions to access this route. Missing: ${missingPerms.join(
", ",
)}`,
},
403,
error.headers.toJSON(),
);
} }
} } else if (value instanceof File) {
if (!files.has(key)) {
if (auth?.user) { files.set(key, value);
return {
user: auth.user as User,
token: auth.token as string,
application: auth.application as Application | null,
};
}
if (authData.required) {
return context.json(
{
error: "Unauthorized",
},
401,
error.headers.toJSON(),
);
}
if (
authData.requiredOnMethods?.includes(context.req.method as HttpVerb)
) {
return context.json(
{
error: "Unauthorized",
},
401,
error.headers.toJSON(),
);
}
return {
user: null,
token: null,
application: null,
};
}); */
/**
* Middleware to magically unfuck forms
* Add it to random Hono routes and hope it works
* @returns
*/
export const qs = () => {
return createMiddleware(async (context, next) => {
const contentType = context.req.header("content-type");
if (contentType?.includes("multipart/form-data")) {
// Get it as a query format to pass on to qs, then insert back files
const formData = await context.req.formData();
const urlparams = new URLSearchParams();
const files = new Map<string, File>();
for (const [key, value] of [...formData.entries()]) {
if (Array.isArray(value)) {
for (const val of value) {
urlparams.append(key, val);
}
} else if (value instanceof File) {
if (!files.has(key)) {
files.set(key, value);
}
} else {
urlparams.append(key, String(value));
}
} }
} else {
const parsed = parse(urlparams.toString(), { urlparams.append(key, String(value));
parseArrays: true,
interpretNumericEntities: true,
});
// @ts-ignore Very bad hack
context.req.parseBody = <T extends BodyData = BodyData>() =>
Promise.resolve({
...parsed,
...Object.fromEntries(files),
} as T);
context.req.formData = () =>
// @ts-ignore I'm so sorry for this
Promise.resolve({
...parsed,
...Object.fromEntries(files),
});
// @ts-ignore I'm so sorry for this
context.req.bodyCache.formData = {
...parsed,
...Object.fromEntries(files),
};
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
const parsed = parse(await context.req.text(), {
parseArrays: true,
interpretNumericEntities: true,
});
context.req.parseBody = <T extends BodyData = BodyData>() =>
Promise.resolve(parsed as T);
// @ts-ignore Very bad hack
context.req.formData = () => Promise.resolve(parsed);
// @ts-ignore I'm so sorry for this
context.req.bodyCache.formData = parsed;
} }
await next(); }
const parsed = parse(urlparams.toString(), {
parseArrays: true,
interpretNumericEntities: true,
}); });
};
return {
parsed,
files,
};
}
// Helper function to parse urlencoded data
async function parseUrlEncoded(context: Context) {
const parsed = parse(await context.req.text(), {
parseArrays: true,
interpretNumericEntities: true,
});
return parsed;
}
export const qsQuery = () => { export const qsQuery = () => {
return createMiddleware(async (context, next) => { return createMiddleware(async (context, next) => {
@ -350,77 +246,49 @@ export const qsQuery = () => {
// @ts-ignore Very bad hack // @ts-ignore Very bad hack
context.req.query = () => parsed; context.req.query = () => parsed;
// @ts-ignore I'm so sorry for this // @ts-ignore I'm so sorry for this
context.req.queries = () => parsed; context.req.queries = () => parsed;
await next(); await next();
}); });
}; };
// Fill in queries, formData and json export const setContextFormDataToObject = (
context: Context,
setTo: object,
): Context => {
// @ts-expect-error HACK
context.req.bodyCache.formData = setTo;
context.req.parseBody = async () =>
context.req.bodyCache.formData as FormData;
context.req.formData = async () =>
context.req.bodyCache.formData as FormData;
return context;
};
/*
* Middleware to magically unfuck forms
* Add it to random Hono routes and hope it works
* @returns
*/
export const jsonOrForm = () => { export const jsonOrForm = () => {
return createMiddleware(async (context, next) => { return createMiddleware(async (context, next) => {
const contentType = context.req.header("content-type"); const contentType = context.req.header("content-type");
if (contentType?.includes("application/json")) { if (contentType?.includes("application/json")) {
context.req.parseBody = async <T extends BodyData = BodyData>() => setContextFormDataToObject(context, await context.req.json());
(await context.req.json()) as T;
context.req.bodyCache.formData = await context.req.json();
context.req.formData = async () =>
context.req.bodyCache.formData as FormData;
} else if (contentType?.includes("application/x-www-form-urlencoded")) { } else if (contentType?.includes("application/x-www-form-urlencoded")) {
const parsed = parse(await context.req.text(), { const parsed = await parseUrlEncoded(context);
parseArrays: true,
interpretNumericEntities: true,
});
context.req.parseBody = <T extends BodyData = BodyData>() => setContextFormDataToObject(context, parsed);
Promise.resolve(parsed as T);
// @ts-ignore Very bad hack
context.req.formData = () => Promise.resolve(parsed);
// @ts-ignore I'm so sorry for this
context.req.bodyCache.formData = parsed;
} else if (contentType?.includes("multipart/form-data")) { } else if (contentType?.includes("multipart/form-data")) {
// Get it as a query format to pass on to qs, then insert back files const { parsed, files } = await parseFormData(context);
const formData = await context.req.formData();
const urlparams = new URLSearchParams();
const files = new Map<string, File>();
for (const [key, value] of [...formData.entries()]) {
if (Array.isArray(value)) {
for (const val of value) {
urlparams.append(key, val);
}
} else if (value instanceof File) {
if (!files.has(key)) {
files.set(key, value);
}
} else {
urlparams.append(key, String(value));
}
}
const parsed = parse(urlparams.toString(), { setContextFormDataToObject(context, {
parseArrays: true,
interpretNumericEntities: true,
});
// @ts-ignore Very bad hack
context.req.parseBody = <T extends BodyData = BodyData>() =>
Promise.resolve({
...parsed,
...Object.fromEntries(files),
} as T);
context.req.formData = () =>
// @ts-ignore I'm so sorry for this
Promise.resolve({
...parsed,
...Object.fromEntries(files),
});
// @ts-ignore I'm so sorry for this
context.req.bodyCache.formData = {
...parsed, ...parsed,
...Object.fromEntries(files), ...Object.fromEntries(files),
}; });
} }
await next(); await next();