Begin work on refactoring every single route to use new subsystems

This commit is contained in:
Jesse Wierzbinski 2024-03-10 12:48:14 -10:00
parent 3b75f5f0a5
commit 05140f0d6f
No known key found for this signature in database
19 changed files with 128 additions and 183 deletions

18
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"configurations": [
{
"type": "node",
"name": "vscode-jest-tests.v2.lysand",
"request": "launch",
"args": [
"test",
"${jest.testFile}"
],
"cwd": "/home/jessew/Dev/lysand",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "/home/jessew/.bun/bin/bun"
}
]
}

View file

@ -1,3 +1,5 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"jest.jestCommandLine": "/home/jessew/.bun/bin/bun test",
"jest.rootPath": "."
} }

View file

@ -6,7 +6,7 @@
* Fuses both and provides a way to retrieve individual values * Fuses both and provides a way to retrieve individual values
*/ */
import { parse, stringify } from "@iarna/toml"; /* import { parse, stringify } from "@iarna/toml";
import chalk from "chalk"; import chalk from "chalk";
import merge from "merge-deep-ts"; import merge from "merge-deep-ts";
@ -23,10 +23,10 @@ const scanConfig = async () => {
} }
return parse(await config.text()) as ConfigType; return parse(await config.text()) as ConfigType;
}; }; */
// Creates the internal config with nothing in it if it doesnt exist // Creates the internal config with nothing in it if it doesnt exist
const scanInternalConfig = async () => { /* const scanInternalConfig = async () => {
const config = Bun.file(process.cwd() + "/config/config.internal.toml"); const config = Bun.file(process.cwd() + "/config/config.internal.toml");
if (!(await config.exists())) { if (!(await config.exists())) {
@ -397,18 +397,18 @@ export const configDefaults: ConfigType = {
max_coeff: 1, max_coeff: 1,
}, },
custom_ratelimits: {}, custom_ratelimits: {},
}; }; */
export const getConfig = () => { /* export const getConfig = () => {
// Deeply merge configDefaults, config and internalConfig // Deeply merge configDefaults, config and internalConfig
return merge([configDefaults, config, internalConfig]) as any as ConfigType; return merge([configDefaults, config, internalConfig]) as any as ConfigType;
}; };
*/
/** /**
* Sets the internal config * Sets the internal config
* @param newConfig Any part of ConfigType * @param newConfig Any part of ConfigType
*/ */
export const setConfig = async (newConfig: Partial<ConfigType>) => { /* export const setConfig = async (newConfig: Partial<ConfigType>) => {
const newInternalConfig = merge([ const newInternalConfig = merge([
internalConfig, internalConfig,
newConfig, newConfig,
@ -421,16 +421,16 @@ export const setConfig = async (newConfig: Partial<ConfigType>) => {
newInternalConfig as any newInternalConfig as any
)}` )}`
); );
}; }; */
export const getHost = () => { /* export const getHost = () => {
const url = new URL(getConfig().http.base_url); const url = new URL(getConfig().http.base_url);
return url.host; return url.host;
}; };
*/
// Refresh config every 5 seconds // Refresh config every 5 seconds
setInterval(() => { /* setInterval(() => {
scanConfig() scanConfig()
.then(newConfig => { .then(newConfig => {
if (newConfig !== config) { if (newConfig !== config) {
@ -440,6 +440,7 @@ setInterval(() => {
.catch(e => { .catch(e => {
console.error(e); console.error(e);
}); });
}, 5000); }, 5000); */
export { config }; /* export { config };
*/

View file

@ -1,7 +1,5 @@
import { MatchedRoute } from "bun";
import { getConfig, getHost } from "~classes/configmanager";
import { xmlResponse } from "@response"; import { xmlResponse } from "@response";
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -16,18 +14,13 @@ export const meta = applyConfig({
}); });
/** export default apiRoute(async (req, matchedRoute, extraData) => {
* Host meta endpoint const config = await extraData.configManager.getConfig();
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const config = getConfig();
return xmlResponse(` return xmlResponse(`
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="${config.http.base_url}/.well-known/webfinger?resource={uri}"/> <Link rel="lrdd" template="${config.http.base_url}/.well-known/webfinger?resource={uri}"/>
</XRD> </XRD>
`); `);
}; });

View file

@ -1,7 +1,5 @@
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { apiRoute, applyConfig } from "@api";
import { getConfig } from "~classes/configmanager";
import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -15,14 +13,9 @@ export const meta = applyConfig({
route: "/.well-known/lysand", route: "/.well-known/lysand",
}); });
/** export default apiRoute(async (req, matchedRoute, extraData) => {
* Lysand instance metadata endpoint const config = await extraData.configManager.getConfig();
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const config = getConfig();
// In the format acct:name@example.com // In the format acct:name@example.com
return jsonResponse({ return jsonResponse({
type: "ServerMetadata", type: "ServerMetadata",
@ -47,4 +40,4 @@ export default async (
website: "https://lysand.org", website: "https://lysand.org",
// TODO: Add admins, moderators field // TODO: Add admins, moderators field
}) })
}; })

View file

@ -1,6 +1,4 @@
import { MatchedRoute } from "bun"; import { apiRoute, applyConfig } from "@api";
import { getConfig, getHost } from "~classes/configmanager";
import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -15,14 +13,8 @@ export const meta = applyConfig({
}); });
/** export default apiRoute(async (req, matchedRoute, extraData) => {
* Redirect to /nodeinfo/2.0 const config = await extraData.configManager.getConfig();
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const config = getConfig();
return new Response("", { return new Response("", {
status: 301, status: 301,
@ -30,4 +22,4 @@ export default async (
Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`, Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`,
}, },
}); });
}; });

View file

@ -1,7 +1,5 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { apiRoute, applyConfig } from "@api";
import { getConfig, getHost } from "~classes/configmanager";
import { applyConfig } from "@api";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
@ -16,35 +14,30 @@ export const meta = applyConfig({
route: "/.well-known/webfinger", route: "/.well-known/webfinger",
}); });
/** export default apiRoute(async (req, matchedRoute, extraData) => {
* ActivityPub WebFinger endpoint
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
// In the format acct:name@example.com // In the format acct:name@example.com
const resource = matchedRoute.query.resource; const resource = matchedRoute.query.resource;
const requestedUser = resource.split("acct:")[1]; const requestedUser = resource.split("acct:")[1];
const config = getConfig(); const config = await extraData.configManager.getConfig();
const host = new URL(config.http.base_url).hostname;
// Check if user is a local user // Check if user is a local user
if (requestedUser.split("@")[1] !== getHost()) { if (requestedUser.split("@")[1] !== host) {
return errorResponse("User is a remote user", 404); return errorResponse("User is a remote user", 404);
} }
const user = await client.user.findUnique({ const user = await client.user.findUnique({
where: { username: requestedUser.split("@")[0] }, where: { username: requestedUser.split("@")[0] },
}); });
if (!user) { if (!user) {
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
return jsonResponse({ return jsonResponse({
subject: `acct:${user.username}@${getHost()}`, subject: `acct:${user.username}@${host}`,
links: [ links: [
{ {
rel: "self", rel: "self",
@ -63,4 +56,4 @@ export default async (
} }
] ]
}) })
}; });

View file

@ -1,12 +1,10 @@
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import type { MatchedRoute } from "bun";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { TokenType } from "~database/entities/Token"; import { TokenType } from "~database/entities/Token";
import { userRelations } from "~database/entities/User"; import { userRelations } from "~database/entities/User";
import type { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
ratelimits: { ratelimits: {
max: 4, max: 4,
@ -21,10 +19,10 @@ export const meta: APIRouteMeta = applyConfig({
/** /**
* OAuth Code flow * OAuth Code flow
*/ */
export default async ( export default apiRoute<{
req: Request, email: string;
matchedRoute: MatchedRoute password: string;
): Promise<Response> => { }>(async (req, matchedRoute, extraData) => {
const scopes = (matchedRoute.query.scope || "") const scopes = (matchedRoute.query.scope || "")
.replaceAll("+", " ") .replaceAll("+", " ")
.split(" "); .split(" ");
@ -32,10 +30,7 @@ export default async (
const response_type = matchedRoute.query.response_type; const response_type = matchedRoute.query.response_type;
const client_id = matchedRoute.query.client_id; const client_id = matchedRoute.query.client_id;
const formData = await req.formData(); const { email, password } = extraData.parsedRequest;
const email = formData.get("email")?.toString() || null;
const password = formData.get("password")?.toString() || null;
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( Response.redirect(
@ -96,4 +91,4 @@ export default async (
// Redirect back to application // Redirect back to application
return Response.redirect(`${redirect_uri}?code=${code}`, 302); return Response.redirect(`${redirect_uri}?code=${code}`, 302);
}; });

View file

@ -1,6 +1,5 @@
import { errorResponse } from "@response"; import { errorResponse } from "@response";
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import type { MatchedRoute } from "bun";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -14,10 +13,7 @@ export const meta = applyConfig({
}, },
}); });
export default async ( export default apiRoute(async (req, matchedRoute) => {
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
// TODO: Add checks for disabled or not email verified accounts // TODO: Add checks for disabled or not email verified accounts
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
@ -46,4 +42,4 @@ export default async (
"Content-Range": `bytes ${start}-${end}/${file.size}`, "Content-Range": `bytes ${start}-${end}/${file.size}`,
}, },
}); });
}; });

View file

@ -1,4 +1,4 @@
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
@ -16,8 +16,7 @@ export const meta = applyConfig({
/** /**
* ActivityPub nodeinfo 2.0 endpoint * ActivityPub nodeinfo 2.0 endpoint
*/ */
// eslint-disable-next-line @typescript-eslint/require-await export default apiRoute(() => {
export default async (): Promise<Response> => {
// TODO: Implement this // TODO: Implement this
return jsonResponse({ return jsonResponse({
version: "2.0", version: "2.0",
@ -31,4 +30,4 @@ export default async (): Promise<Response> => {
openRegistrations: false, openRegistrations: false,
metadata: {}, metadata: {},
}); });
}; });

View file

@ -1,7 +1,5 @@
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { getConfig } from "~classes/configmanager";
import { oauthRedirectUri } from "@constants"; import { oauthRedirectUri } from "@constants";
import type { MatchedRoute } from "bun";
import { import {
calculatePKCECodeChallenge, calculatePKCECodeChallenge,
discoveryRequest, discoveryRequest,
@ -25,11 +23,7 @@ export const meta = applyConfig({
/** /**
* Redirects the user to the external OAuth provider * Redirects the user to the external OAuth provider
*/ */
export default async ( export default apiRoute(async (req, matchedRoute, extraData) => {
req: Request,
matchedRoute: MatchedRoute
// eslint-disable-next-line @typescript-eslint/require-await
): Promise<Response> => {
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( Response.redirect(
`/oauth/authorize?` + `/oauth/authorize?` +
@ -49,7 +43,7 @@ export default async (
return redirectToLogin("Missing client_id"); return redirectToLogin("Missing client_id");
} }
const config = getConfig(); const config = await extraData.configManager.getConfig();
const issuer = config.oidc.providers.find( const issuer = config.oidc.providers.find(
provider => provider.id === issuerId provider => provider.id === issuerId
@ -98,4 +92,4 @@ export default async (
}).toString(), }).toString(),
302 302
); );
}; });

View file

@ -1,7 +1,5 @@
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { getConfig } from "~classes/configmanager";
import { oauthRedirectUri } from "@constants"; import { oauthRedirectUri } from "@constants";
import type { MatchedRoute } from "bun";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { import {
authorizationCodeGrantRequest, authorizationCodeGrantRequest,
@ -33,10 +31,7 @@ export const meta = applyConfig({
/** /**
* Redirects the user to the external OAuth provider * Redirects the user to the external OAuth provider
*/ */
export default async ( export default apiRoute(async (req, matchedRoute, extraData) => {
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( Response.redirect(
`/oauth/authorize?` + `/oauth/authorize?` +
@ -65,7 +60,7 @@ export default async (
return redirectToLogin("Invalid flow"); return redirectToLogin("Invalid flow");
} }
const config = getConfig(); const config = await extraData.configManager.getConfig();
const issuer = config.oidc.providers.find( const issuer = config.oidc.providers.find(
provider => provider.id === issuerParam provider => provider.id === issuerParam
@ -192,4 +187,4 @@ export default async (
`${flow.application.redirect_uris}?code=${code}`, `${flow.application.redirect_uris}?code=${code}`,
302 302
); );
}; });

View file

@ -1,5 +1,4 @@
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { getConfig } from "~classes/configmanager";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
@ -17,9 +16,8 @@ export const meta = applyConfig({
/** /**
* Lists available OAuth providers * Lists available OAuth providers
*/ */
// eslint-disable-next-line @typescript-eslint/require-await export default apiRoute(async (req, matchedRoute, extraData) => {
export default async (): Promise<Response> => { const config = await extraData.configManager.getConfig();
const config = getConfig();
return jsonResponse( return jsonResponse(
config.oidc.providers.map(p => ({ config.oidc.providers.map(p => ({
@ -28,4 +26,4 @@ export default async (): Promise<Response> => {
id: p.id, id: p.id,
})) }))
); );
}; });

View file

@ -1,5 +1,4 @@
import { applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
@ -18,16 +17,16 @@ export const meta = applyConfig({
/** /**
* Allows getting token from OAuth code * Allows getting token from OAuth code
*/ */
export default async (req: Request): Promise<Response> => { export default apiRoute<{
grant_type: string;
code: string;
redirect_uri: string;
client_id: string;
client_secret: string;
scope: string;
}>(async (req, matchedRoute, extraData) => {
const { grant_type, code, redirect_uri, client_id, client_secret, scope } = const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
await parseRequest<{ extraData.parsedRequest;
grant_type: string;
code: string;
redirect_uri: string;
client_id: string;
client_secret: string;
scope: string;
}>(req);
if (grant_type !== "authorization_code") if (grant_type !== "authorization_code")
return errorResponse( return errorResponse(
@ -61,4 +60,4 @@ export default async (req: Request): Promise<Response> => {
scope: token.scope, scope: token.scope,
created_at: token.created_at, created_at: token.created_at,
}); });
}; });

View file

@ -1,8 +1,5 @@
/* eslint-disable @typescript-eslint/require-await */ import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import type { MatchedRoute } from "bun";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -16,12 +13,6 @@ export const meta = applyConfig({
route: "/object/:id", route: "/object/:id",
}); });
/** export default apiRoute(() => {
* Fetch a user
*/
export default async (
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
return jsonResponse({}); return jsonResponse({});
}; });

View file

@ -1,10 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ // TODO: Refactor into smaller packages
/* eslint-disable @typescript-eslint/no-unused-vars */ import { apiRoute, applyConfig } from "@api";
import { applyConfig } from "@api";
import { getConfig } from "~classes/configmanager";
import { getBestContentType } from "@content_types"; import { getBestContentType } from "@content_types";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import type { MatchedRoute } from "bun";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { parseEmojis } from "~database/entities/Emoji"; import { parseEmojis } from "~database/entities/Emoji";
import { createLike, deleteLike } from "~database/entities/Like"; import { createLike, deleteLike } from "~database/entities/Like";
@ -39,13 +36,10 @@ export const meta = applyConfig({
/** /**
* ActivityPub user inbox endpoint * ActivityPub user inbox endpoint
*/ */
export default async ( export default apiRoute(async (req, matchedRoute, extraData) => {
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const username = matchedRoute.params.username; const username = matchedRoute.params.username;
const config = getConfig(); const config = await extraData.configManager.getConfig();
try { try {
if ( if (
@ -313,7 +307,7 @@ export default async (
} }
// Create new reblog // Create new reblog
const newReblog = await client.status.create({ await client.status.create({
data: { data: {
authorId: author.id, authorId: author.id,
reblogId: rebloggedStatus.id, reblogId: rebloggedStatus.id,
@ -405,4 +399,4 @@ export default async (
} }
return jsonResponse({}); return jsonResponse({});
}; });

View file

@ -1,9 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { applyConfig } from "@api";
import { getConfig } from "~classes/configmanager";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import type { MatchedRoute } from "bun";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userRelations, userToLysand } from "~database/entities/User"; import { userRelations, userToLysand } from "~database/entities/User";
@ -22,14 +18,9 @@ export const meta = applyConfig({
/** /**
* ActivityPub user inbox endpoint * ActivityPub user inbox endpoint
*/ */
export default async ( export default apiRoute(async (req, matchedRoute) => {
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const uuid = matchedRoute.params.uuid; const uuid = matchedRoute.params.uuid;
const config = getConfig();
const user = await client.user.findUnique({ const user = await client.user.findUnique({
where: { where: {
id: uuid, id: uuid,
@ -42,4 +33,4 @@ export default async (
} }
return jsonResponse(userToLysand(user)); return jsonResponse(userToLysand(user));
}; });

View file

@ -1,7 +1,5 @@
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import type { MatchedRoute } from "bun"; import { apiRoute, applyConfig } from "@api";
import { getConfig, getHost } from "~classes/configmanager";
import { applyConfig } from "@api";
import { import {
statusAndUserRelations, statusAndUserRelations,
statusToLysand, statusToLysand,
@ -23,13 +21,11 @@ export const meta = applyConfig({
/** /**
* ActivityPub user outbox endpoint * ActivityPub user outbox endpoint
*/ */
export default async ( export default apiRoute(async (req, matchedRoute, extraData) => {
req: Request,
matchedRoute: MatchedRoute
): Promise<Response> => {
const uuid = matchedRoute.params.uuid; const uuid = matchedRoute.params.uuid;
const pageNumber = Number(matchedRoute.query.page) || 1; const pageNumber = Number(matchedRoute.query.page) || 1;
const config = getConfig(); const config = await extraData.configManager.getConfig();
const host = new URL(config.http.base_url).hostname;
const statuses = await client.status.findMany({ const statuses = await client.status.findMany({
where: { where: {
@ -53,19 +49,19 @@ export default async (
}); });
return jsonResponse({ return jsonResponse({
first: `${getHost()}/users/${uuid}/outbox?page=1`, first: `${host}/users/${uuid}/outbox?page=1`,
last: `${getHost()}/users/${uuid}/outbox?page=1`, last: `${host}/users/${uuid}/outbox?page=1`,
total_items: totalStatuses, total_items: totalStatuses,
// Server actor // Server actor
author: `${config.http.base_url}/users/actor`, author: `${config.http.base_url}/users/actor`,
next: next:
statuses.length === 20 statuses.length === 20
? `${getHost()}/users/${uuid}/outbox?page=${pageNumber + 1}` ? `${host}/users/${uuid}/outbox?page=${pageNumber + 1}`
: undefined, : undefined,
prev: prev:
pageNumber > 1 pageNumber > 1
? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}` ? `${host}/users/${uuid}/outbox?page=${pageNumber - 1}`
: undefined, : undefined,
items: statuses.map(s => statusToLysand(s)), items: statuses.map(s => statusToLysand(s)),
}); });
}; });

View file

@ -1,4 +1,5 @@
import { getConfig } from "~classes/configmanager"; import { getConfig } from "~classes/configmanager";
import type { RouteHandler } from "~server/api/routes.type";
import type { APIRouteMeta } from "~types/api"; import type { APIRouteMeta } from "~types/api";
export const applyConfig = (routeMeta: APIRouteMeta) => { export const applyConfig = (routeMeta: APIRouteMeta) => {
@ -16,3 +17,7 @@ export const applyConfig = (routeMeta: APIRouteMeta) => {
return newMeta; return newMeta;
}; };
export const apiRoute = <T>(routeFunction: RouteHandler<T>) => {
return routeFunction;
};