From 78f216092b11a8c1002fd653672df355f37b430f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 7 Mar 2024 19:34:50 -1000 Subject: [PATCH] refactor: Rewrite functions into packages --- bun.lockb | Bin 423384 -> 424080 bytes index.ts | 2 - package.json | 4 +- packages/cli-parser/cli-builder.type.ts | 8 + packages/cli-parser/index.ts | 203 ++++++++++ packages/cli-parser/package.json | 6 + packages/cli-parser/tests/cli-builder.test.ts | 217 +++++++++++ packages/config-manager/config-type.type.ts | 359 ++++++++++++++++++ packages/config-manager/index.ts | 118 ++++++ packages/config-manager/package.json | 6 + .../tests/config-manager.test.ts | 96 +++++ packages/request-parser/index.ts | 170 +++++++++ packages/request-parser/package.json | 6 + .../tests/request-parser.test.ts | 158 ++++++++ server/api/api/v1/accounts/index.ts | 32 +- server/api/api/v1/accounts/search/index.ts | 30 +- .../v1/accounts/update_credentials/index.ts | 52 +-- .../v1/accounts/verify_credentials/index.ts | 11 +- server/api/routes.type.ts | 13 + tsconfig.json | 2 +- utils/request.ts | 3 +- 21 files changed, 1426 insertions(+), 70 deletions(-) create mode 100644 packages/cli-parser/cli-builder.type.ts create mode 100644 packages/cli-parser/index.ts create mode 100644 packages/cli-parser/package.json create mode 100644 packages/cli-parser/tests/cli-builder.test.ts create mode 100644 packages/config-manager/config-type.type.ts create mode 100644 packages/config-manager/index.ts create mode 100644 packages/config-manager/package.json create mode 100644 packages/config-manager/tests/config-manager.test.ts create mode 100644 packages/request-parser/index.ts create mode 100644 packages/request-parser/package.json create mode 100644 packages/request-parser/tests/request-parser.test.ts create mode 100644 server/api/routes.type.ts diff --git a/bun.lockb b/bun.lockb index f33d46fbcbf29c17ec632f6bb3d3796a7f0e979c..04d28da78f7fb0f0032580fef15f663800caf99b 100755 GIT binary patch delta 81308 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 X-jOBYnJbGAKW!g2nJ0bXP5gfV&)tVf diff --git a/index.ts b/index.ts index c096a408..6a002674 100644 --- a/index.ts +++ b/index.ts @@ -162,8 +162,6 @@ const logRequest = async (req: Request) => { ); // Add headers - // @ts-expect-error TypeScript is missing entries for some reason - // eslint-disable-next-line @typescript-eslint/no-unsafe-call const headers = req.headers.entries(); for (const [key, value] of headers) { diff --git a/package.json b/package.json index ecacd291..f1ef982e 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,8 @@ "prisma": "^5.6.0", "prisma-redis-middleware": "^4.8.0", "semver": "^7.5.4", - "sharp": "^0.33.0-rc.2" + "sharp": "^0.33.0-rc.2", + "request-parser": "file:packages/request-parser", + "config-manager": "file:packages/config-manager" } } \ No newline at end of file diff --git a/packages/cli-parser/cli-builder.type.ts b/packages/cli-parser/cli-builder.type.ts new file mode 100644 index 00000000..8cd2bd30 --- /dev/null +++ b/packages/cli-parser/cli-builder.type.ts @@ -0,0 +1,8 @@ +export interface CliParameter { + name: string; + // If not positioned, the argument will need to be called with --name value instead of just value + positioned?: boolean; + // Whether the argument needs a value (requires positioned to be false) + needsValue?: boolean; + type: "string" | "number" | "boolean" | "array"; +} diff --git a/packages/cli-parser/index.ts b/packages/cli-parser/index.ts new file mode 100644 index 00000000..34c31177 --- /dev/null +++ b/packages/cli-parser/index.ts @@ -0,0 +1,203 @@ +import type { CliParameter } from "./cli-builder.type"; + +export function startsWithArray(fullArray: any[], startArray: any[]) { + if (startArray.length > fullArray.length) { + return false; + } + return fullArray + .slice(0, startArray.length) + .every((value, index) => value === startArray[index]); +} + +/** + * Builder for a CLI + * @param commands Array of commands to register + */ +export class CliBuilder { + constructor(public commands: CliCommand[] = []) {} + + /** + * Add command to the CLI + * @throws Error if command already exists + * @param command Command to add + */ + registerCommand(command: CliCommand) { + if (this.checkIfCommandAlreadyExists(command)) { + throw new Error( + `Command category '${command.categories.join(" ")}' already exists` + ); + } + this.commands.push(command); + } + + /** + * Add multiple commands to the CLI + * @throws Error if command already exists + * @param commands Commands to add + */ + registerCommands(commands: CliCommand[]) { + const existingCommand = commands.find(command => + this.checkIfCommandAlreadyExists(command) + ); + if (existingCommand) { + throw new Error( + `Command category '${existingCommand.categories.join(" ")}' already exists` + ); + } + this.commands.push(...commands); + } + + /** + * Remove command from the CLI + * @param command Command to remove + */ + deregisterCommand(command: CliCommand) { + this.commands = this.commands.filter( + registeredCommand => registeredCommand !== command + ); + } + + /** + * Remove multiple commands from the CLI + * @param commands Commands to remove + */ + deregisterCommands(commands: CliCommand[]) { + this.commands = this.commands.filter( + registeredCommand => !commands.includes(registeredCommand) + ); + } + + checkIfCommandAlreadyExists(command: CliCommand) { + return this.commands.some( + registeredCommand => + registeredCommand.categories.length == + command.categories.length && + registeredCommand.categories.every( + (category, index) => category === command.categories[index] + ) + ); + } + + /** + * Get relevant args for the command (without executable or runtime) + * @param args Arguments passed to the CLI + */ + private getRelevantArgs(args: string[]) { + if (args[0].startsWith("./")) { + // Formatted like ./cli.ts [command] + return args.slice(1); + } else if (args[0].includes("bun")) { + // Formatted like bun cli.ts [command] + return args.slice(2); + } else { + return args; + } + } + + /** + * Turn raw system args into a CLI command and run it + * @param args Args directly from process.argv + */ + processArgs(args: string[]) { + const revelantArgs = this.getRelevantArgs(args); + // Find revelant command + // Search for a command with as many categories matching args as possible + const matchingCommands = this.commands.filter(command => + startsWithArray(revelantArgs, command.categories) + ); + + // Get command with largest category size + const command = matchingCommands.reduce((prev, current) => + prev.categories.length > current.categories.length ? prev : current + ); + + const argsWithoutCategories = args.slice(command.categories.length - 1); + + command.run(argsWithoutCategories); + } +} + +/** + * A command that can be executed from the command line + * @param categories Example: `["user", "create"]` for the command `./cli user create --name John` + */ +export class CliCommand { + constructor( + public categories: string[], + public argTypes: CliParameter[], + private execute: (args: Record) => void + ) {} + + /** + * Parses string array arguments into a full JavaScript object + * @param argsWithoutCategories + * @returns + */ + private parseArgs(argsWithoutCategories: string[]): Record { + const parsedArgs: Record = {}; + let currentParameter: CliParameter | null = null; + + for (let i = 0; i < argsWithoutCategories.length; i++) { + const arg = argsWithoutCategories[i]; + + if (arg.startsWith("--")) { + const argName = arg.substring(2); + currentParameter = + this.argTypes.find(argType => argType.name === argName) || + null; + if (currentParameter && !currentParameter.needsValue) { + parsedArgs[argName] = true; + currentParameter = null; + } else if (currentParameter && currentParameter.needsValue) { + parsedArgs[argName] = this.castArgValue( + argsWithoutCategories[i + 1], + currentParameter.type + ); + i++; + currentParameter = null; + } + } else if (currentParameter) { + parsedArgs[currentParameter.name] = this.castArgValue( + arg, + currentParameter.type + ); + currentParameter = null; + } else { + const positionedArgType = this.argTypes.find( + argType => argType.positioned + ); + if (positionedArgType) { + parsedArgs[positionedArgType.name] = this.castArgValue( + arg, + positionedArgType.type + ); + } + } + } + + return parsedArgs; + } + + private castArgValue(value: string, type: CliParameter["type"]): any { + switch (type) { + case "string": + return value; + case "number": + return Number(value); + case "boolean": + return value === "true"; + case "array": + return value.split(","); + default: + return value; + } + } + + /** + * Runs the execute function with the parsed parameters as an argument + */ + run(argsWithoutCategories: string[]) { + const args = this.parseArgs(argsWithoutCategories); + this.execute(args); + } +} diff --git a/packages/cli-parser/package.json b/packages/cli-parser/package.json new file mode 100644 index 00000000..bf582902 --- /dev/null +++ b/packages/cli-parser/package.json @@ -0,0 +1,6 @@ +{ + "name": "arg-parser", + "version": "0.0.0", + "main": "index.ts", + "dependencies": {} +} \ No newline at end of file diff --git a/packages/cli-parser/tests/cli-builder.test.ts b/packages/cli-parser/tests/cli-builder.test.ts new file mode 100644 index 00000000..8c5b3f5c --- /dev/null +++ b/packages/cli-parser/tests/cli-builder.test.ts @@ -0,0 +1,217 @@ +// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts +import { CliCommand, CliBuilder, startsWithArray } from ".."; +import { describe, beforeEach, it, expect, jest } from "bun:test"; + +describe("startsWithArray", () => { + it("should return true when fullArray starts with startArray", () => { + const fullArray = ["a", "b", "c", "d", "e"]; + const startArray = ["a", "b", "c"]; + expect(startsWithArray(fullArray, startArray)).toBe(true); + }); + + it("should return false when fullArray does not start with startArray", () => { + const fullArray = ["a", "b", "c", "d", "e"]; + const startArray = ["b", "c", "d"]; + expect(startsWithArray(fullArray, startArray)).toBe(false); + }); + + it("should return true when startArray is empty", () => { + const fullArray = ["a", "b", "c", "d", "e"]; + const startArray: any[] = []; + expect(startsWithArray(fullArray, startArray)).toBe(true); + }); + + it("should return false when fullArray is shorter than startArray", () => { + const fullArray = ["a", "b", "c"]; + const startArray = ["a", "b", "c", "d", "e"]; + expect(startsWithArray(fullArray, startArray)).toBe(false); + }); +}); + +describe("CliCommand", () => { + let cliCommand: CliCommand; + + beforeEach(() => { + cliCommand = new CliCommand( + ["category1", "category2"], + [ + { name: "arg1", type: "string", needsValue: true }, + { name: "arg2", type: "number", needsValue: true }, + { name: "arg3", type: "boolean", needsValue: false }, + { name: "arg4", type: "array", needsValue: true }, + ], + () => { + // Do nothing + } + ); + }); + + it("should parse string arguments correctly", () => { + const args = cliCommand["parseArgs"]([ + "--arg1", + "value1", + "--arg2", + "42", + "--arg3", + "--arg4", + "value1,value2", + ]); + expect(args).toEqual({ + arg1: "value1", + arg2: 42, + arg3: true, + arg4: ["value1", "value2"], + }); + }); + + it("should cast argument values correctly", () => { + expect(cliCommand["castArgValue"]("42", "number")).toBe(42); + expect(cliCommand["castArgValue"]("true", "boolean")).toBe(true); + expect(cliCommand["castArgValue"]("value1,value2", "array")).toEqual([ + "value1", + "value2", + ]); + }); + + it("should run the execute function with the parsed parameters", () => { + const mockExecute = jest.fn(); + cliCommand = new CliCommand( + ["category1", "category2"], + [ + { name: "arg1", type: "string", needsValue: true }, + { name: "arg2", type: "number", needsValue: true }, + { name: "arg3", type: "boolean", needsValue: false }, + { name: "arg4", type: "array", needsValue: true }, + ], + mockExecute + ); + + cliCommand.run([ + "--arg1", + "value1", + "--arg2", + "42", + "--arg3", + "--arg4", + "value1,value2", + ]); + expect(mockExecute).toHaveBeenCalledWith({ + arg1: "value1", + arg2: 42, + arg3: true, + arg4: ["value1", "value2"], + }); + }); + + it("should work with a mix of positioned and non-positioned arguments", () => { + const mockExecute = jest.fn(); + cliCommand = new CliCommand( + ["category1", "category2"], + [ + { name: "arg1", type: "string", needsValue: true }, + { name: "arg2", type: "number", needsValue: true }, + { name: "arg3", type: "boolean", needsValue: false }, + { name: "arg4", type: "array", needsValue: true }, + { + name: "arg5", + type: "string", + needsValue: true, + positioned: true, + }, + ], + mockExecute + ); + + cliCommand.run([ + "--arg1", + "value1", + "--arg2", + "42", + "--arg3", + "--arg4", + "value1,value2", + "value5", + ]); + + expect(mockExecute).toHaveBeenCalledWith({ + arg1: "value1", + arg2: 42, + arg3: true, + arg4: ["value1", "value2"], + arg5: "value5", + }); + }); +}); + +describe("CliBuilder", () => { + let cliBuilder: CliBuilder; + let mockCommand1: CliCommand; + let mockCommand2: CliCommand; + + beforeEach(() => { + mockCommand1 = new CliCommand(["category1"], [], jest.fn()); + mockCommand2 = new CliCommand(["category2"], [], jest.fn()); + cliBuilder = new CliBuilder([mockCommand1]); + }); + + it("should register a command correctly", () => { + cliBuilder.registerCommand(mockCommand2); + expect(cliBuilder.commands).toContain(mockCommand2); + }); + + it("should register multiple commands correctly", () => { + const mockCommand3 = new CliCommand(["category3"], [], jest.fn()); + cliBuilder.registerCommands([mockCommand2, mockCommand3]); + expect(cliBuilder.commands).toContain(mockCommand2); + expect(cliBuilder.commands).toContain(mockCommand3); + }); + + it("should error when adding duplicates", () => { + expect(() => { + cliBuilder.registerCommand(mockCommand1); + }).toThrow(); + + expect(() => { + cliBuilder.registerCommands([mockCommand1]); + }).toThrow(); + }); + + it("should deregister a command correctly", () => { + cliBuilder.deregisterCommand(mockCommand1); + expect(cliBuilder.commands).not.toContain(mockCommand1); + }); + + it("should deregister multiple commands correctly", () => { + cliBuilder.registerCommand(mockCommand2); + cliBuilder.deregisterCommands([mockCommand1, mockCommand2]); + expect(cliBuilder.commands).not.toContain(mockCommand1); + expect(cliBuilder.commands).not.toContain(mockCommand2); + }); + + it("should process args correctly", () => { + const mockExecute = jest.fn(); + const mockCommand = new CliCommand( + ["category1", "sub1"], + [ + { + name: "arg1", + type: "string", + needsValue: true, + positioned: false, + }, + ], + mockExecute + ); + cliBuilder.registerCommand(mockCommand); + cliBuilder.processArgs([ + "./cli.ts", + "category1", + "sub1", + "--arg1", + "value1", + ]); + expect(mockExecute).toHaveBeenCalledWith({ + arg1: "value1", + }); + }); +}); diff --git a/packages/config-manager/config-type.type.ts b/packages/config-manager/config-type.type.ts new file mode 100644 index 00000000..940716d5 --- /dev/null +++ b/packages/config-manager/config-type.type.ts @@ -0,0 +1,359 @@ +export interface ConfigType { + database: { + host: string; + port: number; + username: string; + password: string; + database: string; + }; + + redis: { + queue: { + host: string; + port: number; + password: string; + database: number | null; + }; + cache: { + host: string; + port: number; + password: string; + database: number | null; + enabled: boolean; + }; + }; + + meilisearch: { + host: string; + port: number; + api_key: string; + enabled: boolean; + }; + + signups: { + tos_url: string; + rules: string[]; + registration: boolean; + }; + + oidc: { + providers: { + name: string; + id: string; + url: string; + client_id: string; + client_secret: string; + icon: string; + }[]; + }; + + http: { + base_url: string; + bind: string; + bind_port: string; + banned_ips: string[]; + banned_user_agents: string[]; + }; + + instance: { + name: string; + description: string; + banner: string; + logo: string; + }; + + smtp: { + server: string; + port: number; + username: string; + password: string; + tls: boolean; + }; + + validation: { + max_displayname_size: number; + max_bio_size: number; + max_username_size: number; + max_note_size: number; + max_avatar_size: number; + max_header_size: number; + max_media_size: number; + max_media_attachments: number; + max_media_description_size: number; + max_poll_options: number; + max_poll_option_size: number; + min_poll_duration: number; + max_poll_duration: number; + + username_blacklist: string[]; + blacklist_tempmail: boolean; + email_blacklist: string[]; + url_scheme_whitelist: string[]; + + enforce_mime_types: boolean; + allowed_mime_types: string[]; + }; + + media: { + backend: string; + deduplicate_media: boolean; + conversion: { + convert_images: boolean; + convert_to: string; + }; + }; + + s3: { + endpoint: string; + access_key: string; + secret_access_key: string; + region: string; + bucket_name: string; + public_url: string; + }; + + defaults: { + visibility: string; + language: string; + avatar: string; + header: string; + }; + + email: { + send_on_report: boolean; + send_on_suspend: boolean; + send_on_unsuspend: boolean; + }; + + activitypub: { + use_tombstones: boolean; + reject_activities: string[]; + force_followers_only: string[]; + discard_reports: string[]; + discard_deletes: string[]; + discard_banners: string[]; + discard_avatars: string[]; + discard_updates: string[]; + discard_follows: string[]; + force_sensitive: string[]; + remove_media: string[]; + fetch_all_collection_members: boolean; + authorized_fetch: boolean; + }; + + filters: { + note_filters: string[]; + username_filters: string[]; + displayname_filters: string[]; + bio_filters: string[]; + emoji_filters: string[]; + }; + + logging: { + log_requests: boolean; + log_requests_verbose: boolean; + log_filters: boolean; + }; + + ratelimits: { + duration_coeff: number; + max_coeff: number; + }; + + custom_ratelimits: Record< + string, + { + duration: number; + max: number; + } + >; + [key: string]: unknown; +} + +export const configDefaults: ConfigType = { + http: { + bind: "http://0.0.0.0", + bind_port: "8000", + base_url: "http://lysand.localhost:8000", + banned_ips: [], + banned_user_agents: [], + }, + database: { + host: "localhost", + port: 5432, + username: "postgres", + password: "postgres", + database: "lysand", + }, + redis: { + queue: { + host: "localhost", + port: 6379, + password: "", + database: 0, + }, + cache: { + host: "localhost", + port: 6379, + password: "", + database: 1, + enabled: false, + }, + }, + meilisearch: { + host: "localhost", + port: 1491, + api_key: "", + enabled: false, + }, + signups: { + tos_url: "", + rules: [], + registration: false, + }, + oidc: { + providers: [], + }, + instance: { + banner: "", + description: "", + logo: "", + name: "", + }, + smtp: { + password: "", + port: 465, + server: "", + tls: true, + username: "", + }, + media: { + backend: "local", + deduplicate_media: true, + conversion: { + convert_images: false, + convert_to: "webp", + }, + }, + email: { + send_on_report: false, + send_on_suspend: false, + send_on_unsuspend: false, + }, + s3: { + access_key: "", + bucket_name: "", + endpoint: "", + public_url: "", + region: "", + secret_access_key: "", + }, + validation: { + max_displayname_size: 50, + max_bio_size: 6000, + max_note_size: 5000, + max_avatar_size: 5_000_000, + max_header_size: 5_000_000, + max_media_size: 40_000_000, + max_media_attachments: 10, + max_media_description_size: 1000, + max_poll_options: 20, + max_poll_option_size: 500, + min_poll_duration: 60, + max_poll_duration: 1893456000, + max_username_size: 30, + + username_blacklist: [ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "dev", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "users", + "web", + "search", + "mfa", + ], + + blacklist_tempmail: false, + + email_blacklist: [], + + url_scheme_whitelist: [ + "http", + "https", + "ftp", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "xmpp", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + ], + + enforce_mime_types: false, + allowed_mime_types: [], + }, + defaults: { + visibility: "public", + language: "en", + avatar: "", + header: "", + }, + activitypub: { + use_tombstones: true, + reject_activities: [], + force_followers_only: [], + discard_reports: [], + discard_deletes: [], + discard_banners: [], + discard_avatars: [], + force_sensitive: [], + discard_updates: [], + discard_follows: [], + remove_media: [], + fetch_all_collection_members: false, + authorized_fetch: false, + }, + filters: { + note_filters: [], + username_filters: [], + displayname_filters: [], + bio_filters: [], + emoji_filters: [], + }, + logging: { + log_requests: false, + log_requests_verbose: false, + log_filters: true, + }, + ratelimits: { + duration_coeff: 1, + max_coeff: 1, + }, + custom_ratelimits: {}, +}; diff --git a/packages/config-manager/index.ts b/packages/config-manager/index.ts new file mode 100644 index 00000000..03d1e4fc --- /dev/null +++ b/packages/config-manager/index.ts @@ -0,0 +1,118 @@ +/** + * @file index.ts + * @summary ConfigManager system to retrieve and modify system configuration + * @description Can read from a hand-written file, config.toml, or from a machine-saved file, config.internal.toml + * Fuses both and provides a way to retrieve individual values + */ + +import { parse, stringify, type JsonMap } from "@iarna/toml"; +import type { ConfigType } from "./config-type.type"; +import merge from "merge-deep-ts"; + +export class ConfigManager { + constructor( + public config: { + configPathOverride?: string; + internalConfigPathOverride?: string; + } + ) {} + + /** + * @summary Reads the config files and returns the merge as a JSON object + * @returns {Promise} The merged config file as a JSON object + */ + async getConfig() { + const config = await this.readConfig(); + const internalConfig = await this.readInternalConfig(); + + return this.mergeConfigs(config, internalConfig); + } + + getConfigPath() { + return ( + this.config.configPathOverride || + process.cwd() + "/config/config.toml" + ); + } + + getInternalConfigPath() { + return ( + this.config.internalConfigPathOverride || + process.cwd() + "/config/config.internal.toml" + ); + } + + /** + * @summary Reads the internal config file and returns it as a JSON object + * @returns {Promise} The internal config file as a JSON object + */ + private async readInternalConfig() { + const config = Bun.file(this.getInternalConfigPath()); + + if (!(await config.exists())) { + await Bun.write(config, ""); + } + + return this.parseConfig(await config.text()); + } + + /** + * @summary Reads the config file and returns it as a JSON object + * @returns {Promise} The config file as a JSON object + */ + private async readConfig() { + const config = Bun.file(this.getConfigPath()); + + if (!(await config.exists())) { + throw new Error( + `Error while reading config at path ${this.getConfigPath()}: Config file not found` + ); + } + + return this.parseConfig(await config.text()); + } + + /** + * @summary Parses a TOML string and returns it as a JSON object + * @param text The TOML string to parse + * @returns {T = ConfigType} The parsed TOML string as a JSON object + * @throws {Error} If the TOML string is invalid + * @private + */ + private parseConfig(text: string) { + try { + // To all [Symbol] keys from the object + return JSON.parse(JSON.stringify(parse(text))) as T; + } catch (e: any) { + throw new Error( + `Error while parsing config at path ${this.getConfigPath()}: ${e}` + ); + } + } + + /** + * Writes changed values to the internal config + * @param config The new config object + */ + async writeConfig(config: T) { + const path = this.getInternalConfigPath(); + const file = Bun.file(path); + + await Bun.write( + file, + `# THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT IT MANUALLY, EDIT THE STANDARD CONFIG.TOML INSTEAD.\n${stringify( + config as JsonMap + )}` + ); + } + + /** + * @summary Merges two config objects together, with + * the latter configs' values taking precedence + * @param configs + * @returns + */ + private mergeConfigs(...configs: T[]) { + return merge(configs) as T; + } +} diff --git a/packages/config-manager/package.json b/packages/config-manager/package.json new file mode 100644 index 00000000..e3c7ad60 --- /dev/null +++ b/packages/config-manager/package.json @@ -0,0 +1,6 @@ +{ + "name": "config-manager", + "version": "0.0.0", + "main": "index.ts", + "dependencies": {} +} \ No newline at end of file diff --git a/packages/config-manager/tests/config-manager.test.ts b/packages/config-manager/tests/config-manager.test.ts new file mode 100644 index 00000000..2635aba2 --- /dev/null +++ b/packages/config-manager/tests/config-manager.test.ts @@ -0,0 +1,96 @@ +// FILEPATH: /home/jessew/Dev/lysand/packages/config-manager/config-manager.test.ts +import { stringify } from "@iarna/toml"; +import { ConfigManager } from ".."; +import { describe, beforeEach, spyOn, it, expect } from "bun:test"; + +describe("ConfigManager", () => { + let configManager: ConfigManager; + + beforeEach(() => { + configManager = new ConfigManager({ + configPathOverride: "./config/config.toml", + internalConfigPathOverride: "./config/config.internal.toml", + }); + }); + + it("should get the correct config path", () => { + expect(configManager.getConfigPath()).toEqual("./config/config.toml"); + }); + + it("should get the correct internal config path", () => { + expect(configManager.getInternalConfigPath()).toEqual( + "./config/config.internal.toml" + ); + }); + + it("should read the config file correctly", async () => { + const mockConfig = { key: "value" }; + + // @ts-expect-error This is a mock + spyOn(Bun, "file").mockImplementationOnce(() => ({ + exists: () => + new Promise(resolve => { + resolve(true); + }), + text: () => + new Promise(resolve => { + resolve(stringify(mockConfig)); + }), + })); + + const config = await configManager.getConfig(); + + expect(config).toEqual(mockConfig); + }); + + it("should read the internal config file correctly", async () => { + const mockConfig = { key: "value" }; + + // @ts-expect-error This is a mock + spyOn(Bun, "file").mockImplementationOnce(() => ({ + exists: () => + new Promise(resolve => { + resolve(true); + }), + text: () => + new Promise(resolve => { + resolve(stringify(mockConfig)); + }), + })); + + const config = + // @ts-expect-error Force call private function for testing + await configManager.readInternalConfig(); + + expect(config).toEqual(mockConfig); + }); + + it("should write to the internal config file correctly", async () => { + const mockConfig = { key: "value" }; + + spyOn(Bun, "write").mockImplementationOnce( + () => + new Promise(resolve => { + resolve(10); + }) + ); + + await configManager.writeConfig(mockConfig); + }); + + it("should merge configs correctly", () => { + const config1 = { key1: "value1", key2: "value2" }; + const config2 = { key2: "newValue2", key3: "value3" }; + // @ts-expect-error Force call private function for testing + const mergedConfig = configManager.mergeConfigs>( + config1, + config2 + ); + + expect(mergedConfig).toEqual({ + key1: "value1", + key2: "newValue2", + key3: "value3", + }); + }); +}); diff --git a/packages/request-parser/index.ts b/packages/request-parser/index.ts new file mode 100644 index 00000000..6351fecc --- /dev/null +++ b/packages/request-parser/index.ts @@ -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() { + try { + switch (await this.determineContentType()) { + case "application/json": + return this.parseJson(); + case "application/x-www-form-urlencoded": + return this.parseFormUrlencoded(); + case "multipart/form-data": + return this.parseFormData(); + default: + return this.parseQuery(); + } + } 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(): Promise> { + const formData = await this.request.formData(); + const result: Partial = {}; + + 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(): Promise> { + const formData = await this.request.formData(); + const result: Partial = {}; + + 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(): Promise> { + 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(): Partial { + const result: Partial = {}; + 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; + } +} diff --git a/packages/request-parser/package.json b/packages/request-parser/package.json new file mode 100644 index 00000000..89d30d2c --- /dev/null +++ b/packages/request-parser/package.json @@ -0,0 +1,6 @@ +{ + "name": "request-parser", + "version": "0.0.0", + "main": "index.ts", + "dependencies": {} +} \ No newline at end of file diff --git a/packages/request-parser/tests/request-parser.test.ts b/packages/request-parser/tests/request-parser.test.ts new file mode 100644 index 00000000..d6f4bf20 --- /dev/null +++ b/packages/request-parser/tests/request-parser.test.ts @@ -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" }); + }); + }); +}); diff --git a/server/api/api/v1/accounts/index.ts b/server/api/api/v1/accounts/index.ts index ecd3211c..bd5ec124 100644 --- a/server/api/api/v1/accounts/index.ts +++ b/server/api/api/v1/accounts/index.ts @@ -1,11 +1,11 @@ import { getConfig } from "~classes/configmanager"; -import { parseRequest } from "@request"; import { jsonResponse } from "@response"; import { tempmailDomains } from "@tempmail"; import { applyConfig } from "@api"; import { client } from "~database/datasource"; import { createNewLocalUser } from "~database/entities/User"; import ISO6391 from "iso-639-1"; +import type { RouteHandler } from "~server/api/routes.type"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -19,20 +19,17 @@ export const meta = applyConfig({ }, }); -/** - * Creates a new user - */ -export default async (req: Request): Promise => { +const handler: RouteHandler<{ + username: string; + email: string; + password: string; + agreement: boolean; + locale: string; + reason: string; +}> = async (req, matchedRoute, extraData) => { // TODO: Add Authorization check - const body = await parseRequest<{ - username: string; - email: string; - password: string; - agreement: boolean; - locale: string; - reason: string; - }>(req); + const body = extraData.parsedRequest; const config = getConfig(); @@ -94,8 +91,8 @@ export default async (req: Request): Promise => { // Check if username doesnt match filters if ( - config.filters.username_filters.some( - filter => body.username?.match(filter) + config.filters.username_filters.some(filter => + body.username?.match(filter) ) ) { errors.details.username.push({ @@ -204,3 +201,8 @@ export default async (req: Request): Promise => { status: 200, }); }; + +/** + * Creates a new user + */ +export default handler; diff --git a/server/api/api/v1/accounts/search/index.ts b/server/api/api/v1/accounts/search/index.ts index f523e9fe..3aa51910 100644 --- a/server/api/api/v1/accounts/search/index.ts +++ b/server/api/api/v1/accounts/search/index.ts @@ -1,12 +1,8 @@ import { errorResponse, jsonResponse } from "@response"; -import { - getFromRequest, - userRelations, - userToAPI, -} from "~database/entities/User"; +import { userRelations, userToAPI } from "~database/entities/User"; import { applyConfig } from "@api"; -import { parseRequest } from "@request"; import { client } from "~database/datasource"; +import type { RouteHandler } from "~server/api/routes.type"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -20,10 +16,16 @@ export const meta = applyConfig({ }, }); -export default async (req: Request): Promise => { +const handler: RouteHandler<{ + q?: string; + limit?: number; + offset?: number; + resolve?: boolean; + following?: boolean; +}> = async (req, matchedRoute, extraData) => { // TODO: Add checks for disabled or not email verified accounts - const { user } = await getFromRequest(req); + const { user } = extraData.auth; if (!user) return errorResponse("Unauthorized", 401); @@ -32,13 +34,7 @@ export default async (req: Request): Promise => { limit = 40, offset, q, - } = await parseRequest<{ - q?: string; - limit?: number; - offset?: number; - resolve?: boolean; - following?: boolean; - }>(req); + } = extraData.parsedRequest; if (limit < 1 || limit > 80) { return errorResponse("Limit must be between 1 and 80", 400); @@ -66,7 +62,7 @@ export default async (req: Request): Promise => { ownerId: user.id, following, }, - } + } : undefined, }, take: Number(limit), @@ -76,3 +72,5 @@ export default async (req: Request): Promise => { return jsonResponse(accounts.map(acct => userToAPI(acct))); }; + +export default handler; diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index 2c6ebe96..4ce1142b 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -1,11 +1,6 @@ import { getConfig } from "~classes/configmanager"; -import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; -import { - userRelations, - userToAPI, - type AuthData, -} from "~database/entities/User"; +import { userRelations, userToAPI } from "~database/entities/User"; import { applyConfig } from "@api"; import { sanitize } from "isomorphic-dompurify"; import { sanitizeHtml } from "@sanitization"; @@ -15,7 +10,7 @@ import { parseEmojis } from "~database/entities/Emoji"; import { client } from "~database/datasource"; import type { APISource } from "~types/entities/source"; import { convertTextToHtml } from "@formatting"; -import type { MatchedRoute } from "bun"; +import type { RouteHandler } from "~server/api/routes.type"; export const meta = applyConfig({ allowedMethods: ["PATCH"], @@ -29,15 +24,19 @@ export const meta = applyConfig({ }, }); -/** - * Patches a user - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute, - auth: AuthData -): Promise => { - const { user } = auth; +const handler: RouteHandler<{ + display_name: string; + note: string; + avatar: File; + header: File; + locked: string; + bot: string; + discoverable: string; + "source[privacy]": string; + "source[sensitive]": string; + "source[language]": string; +}> = async (req, matchedRoute, extraData) => { + const { user } = extraData.auth; if (!user) return errorResponse("Unauthorized", 401); @@ -54,18 +53,7 @@ export default async ( "source[privacy]": source_privacy, "source[sensitive]": source_sensitive, "source[language]": source_language, - } = await parseRequest<{ - display_name: string; - note: string; - avatar: File; - header: File; - locked: string; - bot: string; - discoverable: string; - "source[privacy]": string; - "source[sensitive]": string; - "source[language]": string; - }>(req); + } = extraData.parsedRequest; const sanitizedNote = await sanitizeHtml(note ?? ""); @@ -147,7 +135,7 @@ export default async ( } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (user.source as any).privacy = source_privacy; + user.source.privacy = source_privacy; } if (source_sensitive && user.source) { @@ -157,7 +145,7 @@ export default async ( } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (user.source as any).sensitive = source_sensitive === "true"; + user.source.sensitive = source_sensitive === "true"; } if (source_language && user.source) { @@ -169,7 +157,7 @@ export default async ( } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (user.source as any).language = source_language; + user.source.language = source_language; } if (avatar) { @@ -264,3 +252,5 @@ export default async ( return jsonResponse(userToAPI(output)); }; + +export default handler; diff --git a/server/api/api/v1/accounts/verify_credentials/index.ts b/server/api/api/v1/accounts/verify_credentials/index.ts index 94543895..1a56825b 100644 --- a/server/api/api/v1/accounts/verify_credentials/index.ts +++ b/server/api/api/v1/accounts/verify_credentials/index.ts @@ -1,6 +1,7 @@ import { errorResponse, jsonResponse } from "@response"; -import { getFromRequest, userToAPI } from "~database/entities/User"; +import { userToAPI } from "~database/entities/User"; import { applyConfig } from "@api"; +import type { RouteHandler } from "~server/api/routes.type"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -14,10 +15,12 @@ export const meta = applyConfig({ }, }); -export default async (req: Request): Promise => { +const handler: RouteHandler<> = (req, matchedRoute, extraData) => {}; + +const handler: RouteHandler<""> = (req, matchedRoute, extraData) => { // TODO: Add checks for disabled or not email verified accounts - const { user } = await getFromRequest(req); + const { user } = extraData.auth; if (!user) return errorResponse("Unauthorized", 401); @@ -25,3 +28,5 @@ export default async (req: Request): Promise => { ...userToAPI(user, true), }); }; + +export default handler; diff --git a/server/api/routes.type.ts b/server/api/routes.type.ts new file mode 100644 index 00000000..d3cea716 --- /dev/null +++ b/server/api/routes.type.ts @@ -0,0 +1,13 @@ +import type { MatchedRoute } from "bun"; +import type { ConfigManager } from "config-manager"; +import type { AuthData } from "~database/entities/User"; + +export type RouteHandler = ( + req: Request, + matchedRoute: MatchedRoute, + extraData: { + auth: AuthData; + parsedRequest: Partial; + configManager: ConfigManager; + } +) => Response | Promise; diff --git a/tsconfig.json b/tsconfig.json index f9efccf5..ef00e6ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["ESNext", "DOM"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "esnext", "target": "esnext", "moduleResolution": "bundler", diff --git a/utils/request.ts b/utils/request.ts index bd9911fc..8bd59566 100644 --- a/utils/request.ts +++ b/utils/request.ts @@ -5,7 +5,7 @@ * either FormData, query parameters, or JSON in the request * @param request The request to parse */ -export async function parseRequest(request: Request): Promise> { +/* export async function parseRequest(request: Request): Promise> { const query = new URL(request.url).searchParams; let output: Partial = {}; @@ -93,3 +93,4 @@ export async function parseRequest(request: Request): Promise> { return output; } + */