mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
Replace eslint and prettier with Biome
This commit is contained in:
parent
4a5a2ea590
commit
af0d627f19
199 changed files with 16493 additions and 16361 deletions
|
|
@ -1,23 +1,23 @@
|
|||
export interface CliParameter {
|
||||
name: string;
|
||||
/* Like -v for --version */
|
||||
shortName?: string;
|
||||
/**
|
||||
* If not positioned, the argument will need to be called with --name value instead of just value
|
||||
* @default true
|
||||
*/
|
||||
positioned?: boolean;
|
||||
/* Whether the argument needs a value (requires positioned to be false) */
|
||||
needsValue?: boolean;
|
||||
optional?: true;
|
||||
type: CliParameterType;
|
||||
description?: string;
|
||||
name: string;
|
||||
/* Like -v for --version */
|
||||
shortName?: string;
|
||||
/**
|
||||
* If not positioned, the argument will need to be called with --name value instead of just value
|
||||
* @default true
|
||||
*/
|
||||
positioned?: boolean;
|
||||
/* Whether the argument needs a value (requires positioned to be false) */
|
||||
needsValue?: boolean;
|
||||
optional?: true;
|
||||
type: CliParameterType;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export enum CliParameterType {
|
||||
STRING = "string",
|
||||
NUMBER = "number",
|
||||
BOOLEAN = "boolean",
|
||||
ARRAY = "array",
|
||||
EMPTY = "empty",
|
||||
STRING = "string",
|
||||
NUMBER = "number",
|
||||
BOOLEAN = "boolean",
|
||||
ARRAY = "array",
|
||||
EMPTY = "empty",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { CliParameterType, type CliParameter } from "./cli-builder.type";
|
||||
import chalk from "chalk";
|
||||
import strip from "strip-ansi";
|
||||
import { type CliParameter, CliParameterType } 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]);
|
||||
export function startsWithArray(fullArray: string[], startArray: string[]) {
|
||||
if (startArray.length > fullArray.length) {
|
||||
return false;
|
||||
}
|
||||
return fullArray
|
||||
.slice(0, startArray.length)
|
||||
.every((value, index) => value === startArray[index]);
|
||||
}
|
||||
|
||||
interface TreeType {
|
||||
[key: string]: CliCommand | TreeType;
|
||||
[key: string]: CliCommand | TreeType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,178 +20,186 @@ interface TreeType {
|
|||
* @param commands Array of commands to register
|
||||
*/
|
||||
export class CliBuilder {
|
||||
constructor(public commands: CliCommand[] = []) {}
|
||||
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 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);
|
||||
}
|
||||
/**
|
||||
* 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 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)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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]
|
||||
)
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
if (args[0].includes("bun")) {
|
||||
// Formatted like bun cli.ts [command]
|
||||
return args.slice(2);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn raw system args into a CLI command and run it
|
||||
* @param args Args directly from process.argv
|
||||
*/
|
||||
async processArgs(args: string[]) {
|
||||
const revelantArgs = this.getRelevantArgs(args);
|
||||
/**
|
||||
* Turn raw system args into a CLI command and run it
|
||||
* @param args Args directly from process.argv
|
||||
*/
|
||||
async processArgs(args: string[]) {
|
||||
const revelantArgs = this.getRelevantArgs(args);
|
||||
|
||||
// Handle "-h", "--help" and "help" commands as special cases
|
||||
if (revelantArgs.length === 1) {
|
||||
if (["-h", "--help", "help"].includes(revelantArgs[0])) {
|
||||
this.displayHelp();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Handle "-h", "--help" and "help" commands as special cases
|
||||
if (revelantArgs.length === 1) {
|
||||
if (["-h", "--help", "help"].includes(revelantArgs[0])) {
|
||||
this.displayHelp();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
);
|
||||
// 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),
|
||||
);
|
||||
|
||||
if (matchingCommands.length === 0) {
|
||||
console.log(
|
||||
`Invalid command "${revelantArgs.join(" ")}". Please use the ${chalk.bold("help")} command to see a list of commands`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
if (matchingCommands.length === 0) {
|
||||
console.log(
|
||||
`Invalid command "${revelantArgs.join(
|
||||
" ",
|
||||
)}". Please use the ${chalk.bold(
|
||||
"help",
|
||||
)} command to see a list of commands`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get command with largest category size
|
||||
const command = matchingCommands.reduce((prev, current) =>
|
||||
prev.categories.length > current.categories.length ? prev : current
|
||||
);
|
||||
// Get command with largest category size
|
||||
const command = matchingCommands.reduce((prev, current) =>
|
||||
prev.categories.length > current.categories.length ? prev : current,
|
||||
);
|
||||
|
||||
const argsWithoutCategories = revelantArgs.slice(
|
||||
command.categories.length
|
||||
);
|
||||
const argsWithoutCategories = revelantArgs.slice(
|
||||
command.categories.length,
|
||||
);
|
||||
|
||||
return await command.run(argsWithoutCategories);
|
||||
}
|
||||
return await command.run(argsWithoutCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively urns the commands into a tree where subcategories mark each sub-branch
|
||||
* @example
|
||||
* ```txt
|
||||
* user verify
|
||||
* user delete
|
||||
* user new admin
|
||||
* user new
|
||||
* ->
|
||||
* user
|
||||
* verify
|
||||
* delete
|
||||
* new
|
||||
* admin
|
||||
* ""
|
||||
* ```
|
||||
*/
|
||||
getCommandTree(commands: CliCommand[]): TreeType {
|
||||
const tree: TreeType = {};
|
||||
/**
|
||||
* Recursively urns the commands into a tree where subcategories mark each sub-branch
|
||||
* @example
|
||||
* ```txt
|
||||
* user verify
|
||||
* user delete
|
||||
* user new admin
|
||||
* user new
|
||||
* ->
|
||||
* user
|
||||
* verify
|
||||
* delete
|
||||
* new
|
||||
* admin
|
||||
* ""
|
||||
* ```
|
||||
*/
|
||||
getCommandTree(commands: CliCommand[]): TreeType {
|
||||
const tree: TreeType = {};
|
||||
|
||||
for (const command of commands) {
|
||||
let currentLevel = tree; // Start at the root
|
||||
for (const command of commands) {
|
||||
let currentLevel = tree; // Start at the root
|
||||
|
||||
// Split the command into parts and iterate over them
|
||||
for (const part of command.categories) {
|
||||
// If this part doesn't exist in the current level of the tree, add it (__proto__ check to prevent prototype pollution)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!currentLevel[part] && part !== "__proto__") {
|
||||
// If this is the last part of the command, add the command itself
|
||||
if (
|
||||
part ===
|
||||
command.categories[command.categories.length - 1]
|
||||
) {
|
||||
currentLevel[part] = command;
|
||||
break;
|
||||
}
|
||||
currentLevel[part] = {};
|
||||
}
|
||||
// Split the command into parts and iterate over them
|
||||
for (const part of command.categories) {
|
||||
// If this part doesn't exist in the current level of the tree, add it (__proto__ check to prevent prototype pollution)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!currentLevel[part] && part !== "__proto__") {
|
||||
// If this is the last part of the command, add the command itself
|
||||
if (
|
||||
part ===
|
||||
command.categories[command.categories.length - 1]
|
||||
) {
|
||||
currentLevel[part] = command;
|
||||
break;
|
||||
}
|
||||
currentLevel[part] = {};
|
||||
}
|
||||
|
||||
// Move down to the next level of the tree
|
||||
currentLevel = currentLevel[part] as TreeType;
|
||||
}
|
||||
}
|
||||
// Move down to the next level of the tree
|
||||
currentLevel = currentLevel[part] as TreeType;
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help for every command in a tree manner
|
||||
*/
|
||||
displayHelp() {
|
||||
/*
|
||||
/**
|
||||
* Display help for every command in a tree manner
|
||||
*/
|
||||
displayHelp() {
|
||||
/*
|
||||
user
|
||||
set
|
||||
admin: List of admin commands
|
||||
|
|
@ -204,217 +212,242 @@ export class CliBuilder {
|
|||
verify
|
||||
...
|
||||
*/
|
||||
const tree = this.getCommandTree(this.commands);
|
||||
let writeBuffer = "";
|
||||
const tree = this.getCommandTree(this.commands);
|
||||
let writeBuffer = "";
|
||||
|
||||
const displayTree = (tree: TreeType, depth = 0) => {
|
||||
for (const [key, value] of Object.entries(tree)) {
|
||||
if (value instanceof CliCommand) {
|
||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}|${chalk.underline(value.description)}\n`;
|
||||
const positionedArgs = value.argTypes.filter(
|
||||
arg => arg.positioned ?? true
|
||||
);
|
||||
const unpositionedArgs = value.argTypes.filter(
|
||||
arg => !(arg.positioned ?? true)
|
||||
);
|
||||
const displayTree = (tree: TreeType, depth = 0) => {
|
||||
for (const [key, value] of Object.entries(tree)) {
|
||||
if (value instanceof CliCommand) {
|
||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(
|
||||
key,
|
||||
)}|${chalk.underline(value.description)}\n`;
|
||||
const positionedArgs = value.argTypes.filter(
|
||||
(arg) => arg.positioned ?? true,
|
||||
);
|
||||
const unpositionedArgs = value.argTypes.filter(
|
||||
(arg) => !(arg.positioned ?? true),
|
||||
);
|
||||
|
||||
for (const arg of positionedArgs) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.green(
|
||||
arg.name
|
||||
)}|${
|
||||
arg.description ?? "(no description)"
|
||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
||||
}
|
||||
for (const arg of unpositionedArgs) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.yellow("--" + arg.name)}${arg.shortName ? ", " + chalk.yellow("-" + arg.shortName) : ""}|${
|
||||
arg.description ?? "(no description)"
|
||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
||||
}
|
||||
for (const arg of positionedArgs) {
|
||||
writeBuffer += `${" ".repeat(
|
||||
depth + 1,
|
||||
)}${chalk.green(arg.name)}|${
|
||||
arg.description ?? "(no description)"
|
||||
} ${arg.optional ? chalk.gray("(optional)") : ""}\n`;
|
||||
}
|
||||
for (const arg of unpositionedArgs) {
|
||||
writeBuffer += `${" ".repeat(
|
||||
depth + 1,
|
||||
)}${chalk.yellow(`--${arg.name}`)}${
|
||||
arg.shortName
|
||||
? `, ${chalk.yellow(`-${arg.shortName}`)}`
|
||||
: ""
|
||||
}|${arg.description ?? "(no description)"} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}\n`;
|
||||
}
|
||||
|
||||
if (value.example) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold("Example:")} ${chalk.bgGray(
|
||||
value.example
|
||||
)}\n`;
|
||||
}
|
||||
} else {
|
||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(key)}\n`;
|
||||
displayTree(value, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (value.example) {
|
||||
writeBuffer += `${" ".repeat(depth + 1)}${chalk.bold(
|
||||
"Example:",
|
||||
)} ${chalk.bgGray(value.example)}\n`;
|
||||
}
|
||||
} else {
|
||||
writeBuffer += `${" ".repeat(depth)}${chalk.blue(
|
||||
key,
|
||||
)}\n`;
|
||||
displayTree(value, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
displayTree(tree);
|
||||
displayTree(tree);
|
||||
|
||||
// Replace all "|" with enough dots so that the text on the left + the dots = the same length
|
||||
const optimal_length = Number(
|
||||
// @ts-expect-error Slightly hacky but works
|
||||
writeBuffer.split("\n").reduce((prev, current) => {
|
||||
// If previousValue is empty
|
||||
if (!prev)
|
||||
return current.includes("|")
|
||||
? current.split("|")[0].length
|
||||
: 0;
|
||||
if (!current.includes("|")) return prev;
|
||||
const [left] = current.split("|");
|
||||
// Strip ANSI color codes or they mess up the length
|
||||
return Math.max(Number(prev), strip(left).length);
|
||||
})
|
||||
);
|
||||
// Replace all "|" with enough dots so that the text on the left + the dots = the same length
|
||||
const optimal_length = Number(
|
||||
writeBuffer
|
||||
.split("\n")
|
||||
// @ts-expect-error I don't know how this works and I don't want to know
|
||||
.reduce((prev, current) => {
|
||||
// If previousValue is empty
|
||||
if (!prev)
|
||||
return current.includes("|")
|
||||
? current.split("|")[0].length
|
||||
: 0;
|
||||
if (!current.includes("|")) return prev;
|
||||
const [left] = current.split("|");
|
||||
// Strip ANSI color codes or they mess up the length
|
||||
return Math.max(Number(prev), Bun.stringWidth(left));
|
||||
}),
|
||||
);
|
||||
|
||||
for (const line of writeBuffer.split("\n")) {
|
||||
const [left, right] = line.split("|");
|
||||
if (!right) {
|
||||
console.log(left);
|
||||
continue;
|
||||
}
|
||||
// Strip ANSI color codes or they mess up the length
|
||||
const dots = ".".repeat(optimal_length + 5 - strip(left).length);
|
||||
console.log(`${left}${dots}${right}`);
|
||||
}
|
||||
}
|
||||
for (const line of writeBuffer.split("\n")) {
|
||||
const [left, right] = line.split("|");
|
||||
if (!right) {
|
||||
console.log(left);
|
||||
continue;
|
||||
}
|
||||
// Strip ANSI color codes or they mess up the length
|
||||
const dots = ".".repeat(optimal_length + 5 - Bun.stringWidth(left));
|
||||
console.log(`${left}${dots}${right}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ExecuteFunction<T> = (
|
||||
instance: CliCommand,
|
||||
args: Partial<T>
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
instance: CliCommand,
|
||||
args: Partial<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
) => Promise<number> | Promise<void> | number | void;
|
||||
|
||||
/**
|
||||
* 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<T = any> {
|
||||
constructor(
|
||||
public categories: string[],
|
||||
public argTypes: CliParameter[],
|
||||
private execute: ExecuteFunction<T>,
|
||||
public description?: string,
|
||||
public example?: string
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display help message for the command
|
||||
* formatted with Chalk and with emojis
|
||||
*/
|
||||
displayHelp() {
|
||||
const positionedArgs = this.argTypes.filter(
|
||||
arg => arg.positioned ?? true
|
||||
);
|
||||
const unpositionedArgs = this.argTypes.filter(
|
||||
arg => !(arg.positioned ?? true)
|
||||
);
|
||||
const helpMessage = `
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
export class CliCommand<T = any> {
|
||||
constructor(
|
||||
public categories: string[],
|
||||
public argTypes: CliParameter[],
|
||||
private execute: ExecuteFunction<T>,
|
||||
public description?: string,
|
||||
public example?: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display help message for the command
|
||||
* formatted with Chalk and with emojis
|
||||
*/
|
||||
displayHelp() {
|
||||
const positionedArgs = this.argTypes.filter(
|
||||
(arg) => arg.positioned ?? true,
|
||||
);
|
||||
const unpositionedArgs = this.argTypes.filter(
|
||||
(arg) => !(arg.positioned ?? true),
|
||||
);
|
||||
const helpMessage = `
|
||||
${chalk.green("📚 Command:")} ${chalk.yellow(this.categories.join(" "))}
|
||||
${this.description ? `${chalk.cyan(this.description)}\n` : ""}
|
||||
${chalk.magenta("🔧 Arguments:")}
|
||||
${positionedArgs
|
||||
.map(
|
||||
arg =>
|
||||
`${chalk.bold(arg.name)}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}`
|
||||
)
|
||||
.join("\n")}
|
||||
.map(
|
||||
(arg) =>
|
||||
`${chalk.bold(arg.name)}: ${chalk.blue(
|
||||
arg.description ?? "(no description)",
|
||||
)} ${arg.optional ? chalk.gray("(optional)") : ""}`,
|
||||
)
|
||||
.join("\n")}
|
||||
${unpositionedArgs
|
||||
.map(
|
||||
arg =>
|
||||
`--${chalk.bold(arg.name)}${arg.shortName ? `, -${arg.shortName}` : ""}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}`
|
||||
)
|
||||
.join(
|
||||
"\n"
|
||||
)}${this.example ? `\n${chalk.magenta("🚀 Example:")}\n${chalk.bgGray(this.example)}` : ""}
|
||||
.map(
|
||||
(arg) =>
|
||||
`--${chalk.bold(arg.name)}${
|
||||
arg.shortName ? `, -${arg.shortName}` : ""
|
||||
}: ${chalk.blue(arg.description ?? "(no description)")} ${
|
||||
arg.optional ? chalk.gray("(optional)") : ""
|
||||
}`,
|
||||
)
|
||||
.join("\n")}${
|
||||
this.example
|
||||
? `\n${chalk.magenta("🚀 Example:")}\n${chalk.bgGray(this.example)}`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
console.log(helpMessage);
|
||||
}
|
||||
console.log(helpMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses string array arguments into a full JavaScript object
|
||||
* @param argsWithoutCategories
|
||||
* @returns
|
||||
*/
|
||||
private parseArgs(argsWithoutCategories: string[]): Record<string, any> {
|
||||
const parsedArgs: Record<string, any> = {};
|
||||
let currentParameter: CliParameter | null = null;
|
||||
/**
|
||||
* Parses string array arguments into a full JavaScript object
|
||||
* @param argsWithoutCategories
|
||||
* @returns
|
||||
*/
|
||||
private parseArgs(
|
||||
argsWithoutCategories: string[],
|
||||
): Record<string, string | number | boolean | string[]> {
|
||||
const parsedArgs: Record<string, string | number | boolean | string[]> =
|
||||
{};
|
||||
let currentParameter: CliParameter | null = null;
|
||||
|
||||
for (let i = 0; i < argsWithoutCategories.length; i++) {
|
||||
const arg = argsWithoutCategories[i];
|
||||
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 (arg.startsWith("-")) {
|
||||
const shortName = arg.substring(1);
|
||||
const argType = this.argTypes.find(
|
||||
argType => argType.shortName === shortName
|
||||
);
|
||||
if (argType && !argType.needsValue) {
|
||||
parsedArgs[argType.name] = true;
|
||||
} else if (argType && argType.needsValue) {
|
||||
parsedArgs[argType.name] = this.castArgValue(
|
||||
argsWithoutCategories[i + 1],
|
||||
argType.type
|
||||
);
|
||||
i++;
|
||||
}
|
||||
} else if (currentParameter) {
|
||||
parsedArgs[currentParameter.name] = this.castArgValue(
|
||||
arg,
|
||||
currentParameter.type
|
||||
);
|
||||
currentParameter = null;
|
||||
} else {
|
||||
const positionedArgType = this.argTypes.find(
|
||||
argType => argType.positioned && !parsedArgs[argType.name]
|
||||
);
|
||||
if (positionedArgType) {
|
||||
parsedArgs[positionedArgType.name] = this.castArgValue(
|
||||
arg,
|
||||
positionedArgType.type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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?.needsValue) {
|
||||
parsedArgs[argName] = this.castArgValue(
|
||||
argsWithoutCategories[i + 1],
|
||||
currentParameter.type,
|
||||
);
|
||||
i++;
|
||||
currentParameter = null;
|
||||
}
|
||||
} else if (arg.startsWith("-")) {
|
||||
const shortName = arg.substring(1);
|
||||
const argType = this.argTypes.find(
|
||||
(argType) => argType.shortName === shortName,
|
||||
);
|
||||
if (argType && !argType.needsValue) {
|
||||
parsedArgs[argType.name] = true;
|
||||
} else if (argType?.needsValue) {
|
||||
parsedArgs[argType.name] = this.castArgValue(
|
||||
argsWithoutCategories[i + 1],
|
||||
argType.type,
|
||||
);
|
||||
i++;
|
||||
}
|
||||
} else if (currentParameter) {
|
||||
parsedArgs[currentParameter.name] = this.castArgValue(
|
||||
arg,
|
||||
currentParameter.type,
|
||||
);
|
||||
currentParameter = null;
|
||||
} else {
|
||||
const positionedArgType = this.argTypes.find(
|
||||
(argType) =>
|
||||
argType.positioned && !parsedArgs[argType.name],
|
||||
);
|
||||
if (positionedArgType) {
|
||||
parsedArgs[positionedArgType.name] = this.castArgValue(
|
||||
arg,
|
||||
positionedArgType.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsedArgs;
|
||||
}
|
||||
return parsedArgs;
|
||||
}
|
||||
|
||||
private castArgValue(value: string, type: CliParameter["type"]): any {
|
||||
switch (type) {
|
||||
case CliParameterType.STRING:
|
||||
return value;
|
||||
case CliParameterType.NUMBER:
|
||||
return Number(value);
|
||||
case CliParameterType.BOOLEAN:
|
||||
return value === "true";
|
||||
case CliParameterType.ARRAY:
|
||||
return value.split(",");
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
private castArgValue(
|
||||
value: string,
|
||||
type: CliParameter["type"],
|
||||
): string | number | boolean | string[] {
|
||||
switch (type) {
|
||||
case CliParameterType.STRING:
|
||||
return value;
|
||||
case CliParameterType.NUMBER:
|
||||
return Number(value);
|
||||
case CliParameterType.BOOLEAN:
|
||||
return value === "true";
|
||||
case CliParameterType.ARRAY:
|
||||
return value.split(",");
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the execute function with the parsed parameters as an argument
|
||||
*/
|
||||
async run(argsWithoutCategories: string[]) {
|
||||
const args = this.parseArgs(argsWithoutCategories);
|
||||
return await this.execute(this, args as any);
|
||||
}
|
||||
/**
|
||||
* Runs the execute function with the parsed parameters as an argument
|
||||
*/
|
||||
async run(argsWithoutCategories: string[]) {
|
||||
const args = this.parseArgs(argsWithoutCategories);
|
||||
return await this.execute(this, args as T);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cli-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "chalk": "^5.3.0", "strip-ansi": "^7.1.0" }
|
||||
}
|
||||
"name": "cli-parser",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "chalk": "^5.3.0", "strip-ansi": "^7.1.0" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,485 +1,488 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
|
||||
import { CliCommand, CliBuilder, startsWithArray } from "..";
|
||||
import { describe, beforeEach, it, expect, jest, spyOn } from "bun:test";
|
||||
import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
|
||||
import stripAnsi from "strip-ansi";
|
||||
// FILEPATH: /home/jessew/Dev/lysand/packages/cli-parser/index.test.ts
|
||||
import { CliBuilder, CliCommand, startsWithArray } from "..";
|
||||
import { CliParameterType } from "../cli-builder.type";
|
||||
|
||||
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 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 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 true when startArray is empty", () => {
|
||||
const fullArray = ["a", "b", "c", "d", "e"];
|
||||
const startArray: string[] = [];
|
||||
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);
|
||||
});
|
||||
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;
|
||||
let cliCommand: CliCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
shortName: "a",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
}
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
shortName: "a",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.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 parse string arguments correctly", () => {
|
||||
// @ts-expect-error Testing private method
|
||||
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 parse short names for arguments too", () => {
|
||||
const args = cliCommand["parseArgs"]([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"-a",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
]);
|
||||
expect(args).toEqual({
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
it("should parse short names for arguments too", () => {
|
||||
// @ts-expect-error Testing private method
|
||||
const args = cliCommand.parseArgs([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"-a",
|
||||
"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", CliParameterType.NUMBER)).toBe(
|
||||
42
|
||||
);
|
||||
expect(
|
||||
cliCommand["castArgValue"]("true", CliParameterType.BOOLEAN)
|
||||
).toBe(true);
|
||||
expect(
|
||||
cliCommand["castArgValue"]("value1,value2", CliParameterType.ARRAY)
|
||||
).toEqual(["value1", "value2"]);
|
||||
});
|
||||
it("should cast argument values correctly", () => {
|
||||
// @ts-expect-error Testing private method
|
||||
expect(cliCommand.castArgValue("42", CliParameterType.NUMBER)).toBe(42);
|
||||
// @ts-expect-error Testing private method
|
||||
expect(cliCommand.castArgValue("true", CliParameterType.BOOLEAN)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
// @ts-expect-error Testing private method
|
||||
cliCommand.castArgValue("value1,value2", CliParameterType.ARRAY),
|
||||
).toEqual(["value1", "value2"]);
|
||||
});
|
||||
|
||||
it("should run the execute function with the parsed parameters", async () => {
|
||||
const mockExecute = jest.fn();
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
);
|
||||
it("should run the execute function with the parsed parameters", async () => {
|
||||
const mockExecute = jest.fn();
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
],
|
||||
mockExecute,
|
||||
);
|
||||
|
||||
await cliCommand.run([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
]);
|
||||
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
await cliCommand.run([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
]);
|
||||
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with a mix of positioned and non-positioned arguments", async () => {
|
||||
const mockExecute = jest.fn();
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg5",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
positioned: true,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
);
|
||||
it("should work with a mix of positioned and non-positioned arguments", async () => {
|
||||
const mockExecute = jest.fn();
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
{
|
||||
name: "arg5",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
positioned: true,
|
||||
},
|
||||
],
|
||||
mockExecute,
|
||||
);
|
||||
|
||||
await cliCommand.run([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
"value5",
|
||||
]);
|
||||
await cliCommand.run([
|
||||
"--arg1",
|
||||
"value1",
|
||||
"--arg2",
|
||||
"42",
|
||||
"--arg3",
|
||||
"--arg4",
|
||||
"value1,value2",
|
||||
"value5",
|
||||
]);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
arg5: "value5",
|
||||
});
|
||||
});
|
||||
expect(mockExecute).toHaveBeenCalledWith(cliCommand, {
|
||||
arg1: "value1",
|
||||
arg2: 42,
|
||||
arg3: true,
|
||||
arg4: ["value1", "value2"],
|
||||
arg5: "value5",
|
||||
});
|
||||
});
|
||||
|
||||
it("should display help message correctly", () => {
|
||||
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
||||
// Do nothing
|
||||
});
|
||||
it("should display help message correctly", () => {
|
||||
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
||||
// Do nothing
|
||||
});
|
||||
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
description: "Argument 1",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
description: "Argument 2",
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
description: "Argument 3",
|
||||
optional: true,
|
||||
positioned: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
description: "Argument 4",
|
||||
positioned: false,
|
||||
},
|
||||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
},
|
||||
"This is a test command",
|
||||
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2"
|
||||
);
|
||||
cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
description: "Argument 1",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: true,
|
||||
description: "Argument 2",
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
description: "Argument 3",
|
||||
optional: true,
|
||||
positioned: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
description: "Argument 4",
|
||||
positioned: false,
|
||||
},
|
||||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
},
|
||||
"This is a test command",
|
||||
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2",
|
||||
);
|
||||
|
||||
cliCommand.displayHelp();
|
||||
cliCommand.displayHelp();
|
||||
|
||||
const loggedString = consoleLogSpy.mock.calls.map(call =>
|
||||
stripAnsi(call[0])
|
||||
)[0];
|
||||
const loggedString = consoleLogSpy.mock.calls.map((call) =>
|
||||
stripAnsi(call[0]),
|
||||
)[0];
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleLogSpy.mockRestore();
|
||||
|
||||
expect(loggedString).toContain("📚 Command: category1 category2");
|
||||
expect(loggedString).toContain("🔧 Arguments:");
|
||||
expect(loggedString).toContain("arg1: Argument 1 (optional)");
|
||||
expect(loggedString).toContain("arg2: Argument 2");
|
||||
expect(loggedString).toContain("--arg3: Argument 3 (optional)");
|
||||
expect(loggedString).toContain("--arg4: Argument 4");
|
||||
expect(loggedString).toContain("🚀 Example:");
|
||||
expect(loggedString).toContain(
|
||||
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2"
|
||||
);
|
||||
});
|
||||
expect(loggedString).toContain("📚 Command: category1 category2");
|
||||
expect(loggedString).toContain("🔧 Arguments:");
|
||||
expect(loggedString).toContain("arg1: Argument 1 (optional)");
|
||||
expect(loggedString).toContain("arg2: Argument 2");
|
||||
expect(loggedString).toContain("--arg3: Argument 3 (optional)");
|
||||
expect(loggedString).toContain("--arg4: Argument 4");
|
||||
expect(loggedString).toContain("🚀 Example:");
|
||||
expect(loggedString).toContain(
|
||||
"category1 category2 --arg1 value1 --arg2 42 arg3 --arg4 value1,value2",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CliBuilder", () => {
|
||||
let cliBuilder: CliBuilder;
|
||||
let mockCommand1: CliCommand;
|
||||
let mockCommand2: CliCommand;
|
||||
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]);
|
||||
});
|
||||
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 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 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();
|
||||
it("should error when adding duplicates", () => {
|
||||
expect(() => {
|
||||
cliBuilder.registerCommand(mockCommand1);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
cliBuilder.registerCommands([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 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 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", async () => {
|
||||
const mockExecute = jest.fn();
|
||||
const mockCommand = new CliCommand(
|
||||
["category1", "sub1"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
positioned: false,
|
||||
},
|
||||
],
|
||||
mockExecute
|
||||
);
|
||||
cliBuilder.registerCommand(mockCommand);
|
||||
await cliBuilder.processArgs([
|
||||
"./cli.ts",
|
||||
"category1",
|
||||
"sub1",
|
||||
"--arg1",
|
||||
"value1",
|
||||
]);
|
||||
expect(mockExecute).toHaveBeenCalledWith(expect.anything(), {
|
||||
arg1: "value1",
|
||||
});
|
||||
});
|
||||
it("should process args correctly", async () => {
|
||||
const mockExecute = jest.fn();
|
||||
const mockCommand = new CliCommand(
|
||||
["category1", "sub1"],
|
||||
[
|
||||
{
|
||||
name: "arg1",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
positioned: false,
|
||||
},
|
||||
],
|
||||
mockExecute,
|
||||
);
|
||||
cliBuilder.registerCommand(mockCommand);
|
||||
await cliBuilder.processArgs([
|
||||
"./cli.ts",
|
||||
"category1",
|
||||
"sub1",
|
||||
"--arg1",
|
||||
"value1",
|
||||
]);
|
||||
expect(mockExecute).toHaveBeenCalledWith(expect.anything(), {
|
||||
arg1: "value1",
|
||||
});
|
||||
});
|
||||
|
||||
describe("should build command tree", () => {
|
||||
let cliBuilder: CliBuilder;
|
||||
let mockCommand1: CliCommand;
|
||||
let mockCommand2: CliCommand;
|
||||
let mockCommand3: CliCommand;
|
||||
let mockCommand4: CliCommand;
|
||||
let mockCommand5: CliCommand;
|
||||
describe("should build command tree", () => {
|
||||
let cliBuilder: CliBuilder;
|
||||
let mockCommand1: CliCommand;
|
||||
let mockCommand2: CliCommand;
|
||||
let mockCommand3: CliCommand;
|
||||
let mockCommand4: CliCommand;
|
||||
let mockCommand5: CliCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCommand1 = new CliCommand(["user", "verify"], [], jest.fn());
|
||||
mockCommand2 = new CliCommand(["user", "delete"], [], jest.fn());
|
||||
mockCommand3 = new CliCommand(
|
||||
["user", "new", "admin"],
|
||||
[],
|
||||
jest.fn()
|
||||
);
|
||||
mockCommand4 = new CliCommand(["user", "new"], [], jest.fn());
|
||||
mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn());
|
||||
cliBuilder = new CliBuilder([
|
||||
mockCommand1,
|
||||
mockCommand2,
|
||||
mockCommand3,
|
||||
mockCommand4,
|
||||
mockCommand5,
|
||||
]);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockCommand1 = new CliCommand(["user", "verify"], [], jest.fn());
|
||||
mockCommand2 = new CliCommand(["user", "delete"], [], jest.fn());
|
||||
mockCommand3 = new CliCommand(
|
||||
["user", "new", "admin"],
|
||||
[],
|
||||
jest.fn(),
|
||||
);
|
||||
mockCommand4 = new CliCommand(["user", "new"], [], jest.fn());
|
||||
mockCommand5 = new CliCommand(["admin", "delete"], [], jest.fn());
|
||||
cliBuilder = new CliBuilder([
|
||||
mockCommand1,
|
||||
mockCommand2,
|
||||
mockCommand3,
|
||||
mockCommand4,
|
||||
mockCommand5,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should build the command tree correctly", () => {
|
||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||
expect(tree).toEqual({
|
||||
user: {
|
||||
verify: mockCommand1,
|
||||
delete: mockCommand2,
|
||||
new: {
|
||||
admin: mockCommand3,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
delete: mockCommand5,
|
||||
},
|
||||
});
|
||||
});
|
||||
it("should build the command tree correctly", () => {
|
||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||
expect(tree).toEqual({
|
||||
user: {
|
||||
verify: mockCommand1,
|
||||
delete: mockCommand2,
|
||||
new: {
|
||||
admin: mockCommand3,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
delete: mockCommand5,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should build the command tree correctly when there are no commands", () => {
|
||||
cliBuilder = new CliBuilder([]);
|
||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||
expect(tree).toEqual({});
|
||||
});
|
||||
it("should build the command tree correctly when there are no commands", () => {
|
||||
cliBuilder = new CliBuilder([]);
|
||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||
expect(tree).toEqual({});
|
||||
});
|
||||
|
||||
it("should build the command tree correctly when there is only one command", () => {
|
||||
cliBuilder = new CliBuilder([mockCommand1]);
|
||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||
expect(tree).toEqual({
|
||||
user: {
|
||||
verify: mockCommand1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
it("should build the command tree correctly when there is only one command", () => {
|
||||
cliBuilder = new CliBuilder([mockCommand1]);
|
||||
const tree = cliBuilder.getCommandTree(cliBuilder.commands);
|
||||
expect(tree).toEqual({
|
||||
user: {
|
||||
verify: mockCommand1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show help menu", () => {
|
||||
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
||||
// Do nothing
|
||||
});
|
||||
it("should show help menu", () => {
|
||||
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {
|
||||
// Do nothing
|
||||
});
|
||||
|
||||
const cliBuilder = new CliBuilder();
|
||||
const cliBuilder = new CliBuilder();
|
||||
|
||||
const cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "name",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
description: "Name of new item",
|
||||
},
|
||||
{
|
||||
name: "delete-previous",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: false,
|
||||
positioned: false,
|
||||
optional: true,
|
||||
description: "Also delete the previous item",
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
},
|
||||
"I love sussy sauces",
|
||||
"emoji add --url https://site.com/image.png"
|
||||
);
|
||||
const cliCommand = new CliCommand(
|
||||
["category1", "category2"],
|
||||
[
|
||||
{
|
||||
name: "name",
|
||||
type: CliParameterType.STRING,
|
||||
needsValue: true,
|
||||
description: "Name of new item",
|
||||
},
|
||||
{
|
||||
name: "delete-previous",
|
||||
type: CliParameterType.NUMBER,
|
||||
needsValue: false,
|
||||
positioned: false,
|
||||
optional: true,
|
||||
description: "Also delete the previous item",
|
||||
},
|
||||
{
|
||||
name: "arg3",
|
||||
type: CliParameterType.BOOLEAN,
|
||||
needsValue: false,
|
||||
},
|
||||
{
|
||||
name: "arg4",
|
||||
type: CliParameterType.ARRAY,
|
||||
needsValue: true,
|
||||
},
|
||||
],
|
||||
() => {
|
||||
// Do nothing
|
||||
},
|
||||
"I love sussy sauces",
|
||||
"emoji add --url https://site.com/image.png",
|
||||
);
|
||||
|
||||
cliBuilder.registerCommand(cliCommand);
|
||||
cliBuilder.displayHelp();
|
||||
cliBuilder.registerCommand(cliCommand);
|
||||
cliBuilder.displayHelp();
|
||||
|
||||
const loggedString = consoleLogSpy.mock.calls
|
||||
.map(call => stripAnsi(call[0]))
|
||||
.join("\n");
|
||||
const loggedString = consoleLogSpy.mock.calls
|
||||
.map((call) => stripAnsi(call[0]))
|
||||
.join("\n");
|
||||
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleLogSpy.mockRestore();
|
||||
|
||||
expect(loggedString).toContain("category1");
|
||||
expect(loggedString).toContain(
|
||||
" category2.................I love sussy sauces"
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" name..................Name of new item"
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" arg3..................(no description)"
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" arg4..................(no description)"
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" --delete-previous.....Also delete the previous item (optional)"
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" Example: emoji add --url https://site.com/image.png"
|
||||
);
|
||||
});
|
||||
expect(loggedString).toContain("category1");
|
||||
expect(loggedString).toContain(
|
||||
" category2.................I love sussy sauces",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" name..................Name of new item",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" arg3..................(no description)",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" arg4..................(no description)",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" --delete-previous.....Also delete the previous item (optional)",
|
||||
);
|
||||
expect(loggedString).toContain(
|
||||
" Example: emoji add --url https://site.com/image.png",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,18 +6,18 @@
|
|||
*/
|
||||
|
||||
import { watchConfig } from "c12";
|
||||
import { defaultConfig, type Config } from "./config.type";
|
||||
import { type Config, defaultConfig } from "./config.type";
|
||||
|
||||
const { config } = await watchConfig<Config>({
|
||||
configFile: "./config/config.toml",
|
||||
defaultConfig: defaultConfig,
|
||||
overrides:
|
||||
(
|
||||
await watchConfig<Config>({
|
||||
configFile: "./config/config.internal.toml",
|
||||
defaultConfig: {} as Config,
|
||||
})
|
||||
).config ?? undefined,
|
||||
configFile: "./config/config.toml",
|
||||
defaultConfig: defaultConfig,
|
||||
overrides:
|
||||
(
|
||||
await watchConfig<Config>({
|
||||
configFile: "./config/config.internal.toml",
|
||||
defaultConfig: {} as Config,
|
||||
})
|
||||
).config ?? undefined,
|
||||
});
|
||||
|
||||
const exportedConfig = config ?? defaultConfig;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "config-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "@iarna/toml": "^2.2.5", "merge-deep-ts": "^1.2.6" }
|
||||
}
|
||||
"name": "config-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "@iarna/toml": "^2.2.5", "merge-deep-ts": "^1.2.6" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { appendFile } from "node:fs/promises";
|
||||
import type { BunFile } from "bun";
|
||||
import { appendFile } from "fs/promises";
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = "debug",
|
||||
INFO = "info",
|
||||
WARNING = "warning",
|
||||
ERROR = "error",
|
||||
CRITICAL = "critical",
|
||||
DEBUG = "debug",
|
||||
INFO = "info",
|
||||
WARNING = "warning",
|
||||
ERROR = "error",
|
||||
CRITICAL = "critical",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -14,161 +14,165 @@ export enum LogLevel {
|
|||
* @param output BunFile of output (can be a normal file or something like Bun.stdout)
|
||||
*/
|
||||
export class LogManager {
|
||||
constructor(private output: BunFile) {
|
||||
void this.write(
|
||||
`--- INIT LogManager at ${new Date().toISOString()} ---`
|
||||
);
|
||||
}
|
||||
constructor(private output: BunFile) {
|
||||
void this.write(
|
||||
`--- INIT LogManager at ${new Date().toISOString()} ---`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to the output
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param message Message to log
|
||||
* @param showTimestamp Whether to show the timestamp in the log
|
||||
*/
|
||||
async log(
|
||||
level: LogLevel,
|
||||
entity: string,
|
||||
message: string,
|
||||
showTimestamp = true
|
||||
) {
|
||||
await this.write(
|
||||
`${showTimestamp ? new Date().toISOString() + " " : ""}[${level.toUpperCase()}] ${entity}: ${message}`
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Logs a message to the output
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param message Message to log
|
||||
* @param showTimestamp Whether to show the timestamp in the log
|
||||
*/
|
||||
async log(
|
||||
level: LogLevel,
|
||||
entity: string,
|
||||
message: string,
|
||||
showTimestamp = true,
|
||||
) {
|
||||
await this.write(
|
||||
`${
|
||||
showTimestamp ? `${new Date().toISOString()} ` : ""
|
||||
}[${level.toUpperCase()}] ${entity}: ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async write(text: string) {
|
||||
if (this.output == Bun.stdout) {
|
||||
await Bun.write(Bun.stdout, text + "\n");
|
||||
} else {
|
||||
if (!(await this.output.exists())) {
|
||||
// Create file if it doesn't exist
|
||||
await Bun.write(this.output, "", {
|
||||
createPath: true,
|
||||
});
|
||||
}
|
||||
await appendFile(this.output.name ?? "", text + "\n");
|
||||
}
|
||||
}
|
||||
private async write(text: string) {
|
||||
if (this.output === Bun.stdout) {
|
||||
await Bun.write(Bun.stdout, `${text}\n`);
|
||||
} else {
|
||||
if (!(await this.output.exists())) {
|
||||
// Create file if it doesn't exist
|
||||
await Bun.write(this.output, "", {
|
||||
createPath: true,
|
||||
});
|
||||
}
|
||||
await appendFile(this.output.name ?? "", `${text}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error to the output, wrapper for log
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param error Error to log
|
||||
*/
|
||||
async logError(level: LogLevel, entity: string, error: Error) {
|
||||
await this.log(level, entity, error.message);
|
||||
}
|
||||
/**
|
||||
* Logs an error to the output, wrapper for log
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param error Error to log
|
||||
*/
|
||||
async logError(level: LogLevel, entity: string, error: Error) {
|
||||
await this.log(level, entity, error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a request to the output
|
||||
* @param req Request to log
|
||||
* @param ip IP of the request
|
||||
* @param logAllDetails Whether to log all details of the request
|
||||
*/
|
||||
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
||||
let string = ip ? `${ip}: ` : "";
|
||||
/**
|
||||
* Logs a request to the output
|
||||
* @param req Request to log
|
||||
* @param ip IP of the request
|
||||
* @param logAllDetails Whether to log all details of the request
|
||||
*/
|
||||
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
||||
let string = ip ? `${ip}: ` : "";
|
||||
|
||||
string += `${req.method} ${req.url}`;
|
||||
string += `${req.method} ${req.url}`;
|
||||
|
||||
if (logAllDetails) {
|
||||
string += `\n`;
|
||||
string += ` [Headers]\n`;
|
||||
// Pretty print headers
|
||||
for (const [key, value] of req.headers.entries()) {
|
||||
string += ` ${key}: ${value}\n`;
|
||||
}
|
||||
if (logAllDetails) {
|
||||
string += "\n";
|
||||
string += " [Headers]\n";
|
||||
// Pretty print headers
|
||||
for (const [key, value] of req.headers.entries()) {
|
||||
string += ` ${key}: ${value}\n`;
|
||||
}
|
||||
|
||||
// Pretty print body
|
||||
string += ` [Body]\n`;
|
||||
const content_type = req.headers.get("Content-Type");
|
||||
// Pretty print body
|
||||
string += " [Body]\n";
|
||||
const content_type = req.headers.get("Content-Type");
|
||||
|
||||
if (content_type && content_type.includes("application/json")) {
|
||||
const json = await req.json();
|
||||
const stringified = JSON.stringify(json, null, 4)
|
||||
.split("\n")
|
||||
.map(line => ` ${line}`)
|
||||
.join("\n");
|
||||
if (content_type?.includes("application/json")) {
|
||||
const json = await req.json();
|
||||
const stringified = JSON.stringify(json, null, 4)
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
|
||||
string += `${stringified}\n`;
|
||||
} else if (
|
||||
content_type &&
|
||||
(content_type.includes("application/x-www-form-urlencoded") ||
|
||||
content_type.includes("multipart/form-data"))
|
||||
) {
|
||||
const formData = await req.formData();
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value.toString().length < 300) {
|
||||
string += ` ${key}: ${value.toString()}\n`;
|
||||
} else {
|
||||
string += ` ${key}: <${value.toString().length} bytes>\n`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const text = await req.text();
|
||||
string += ` ${text}\n`;
|
||||
}
|
||||
}
|
||||
await this.log(LogLevel.INFO, "Request", string);
|
||||
}
|
||||
string += `${stringified}\n`;
|
||||
} else if (
|
||||
content_type &&
|
||||
(content_type.includes("application/x-www-form-urlencoded") ||
|
||||
content_type.includes("multipart/form-data"))
|
||||
) {
|
||||
const formData = await req.formData();
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value.toString().length < 300) {
|
||||
string += ` ${key}: ${value.toString()}\n`;
|
||||
} else {
|
||||
string += ` ${key}: <${
|
||||
value.toString().length
|
||||
} bytes>\n`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const text = await req.text();
|
||||
string += ` ${text}\n`;
|
||||
}
|
||||
}
|
||||
await this.log(LogLevel.INFO, "Request", string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs to multiple LogManager instances at once
|
||||
*/
|
||||
export class MultiLogManager {
|
||||
constructor(private logManagers: LogManager[]) {}
|
||||
constructor(private logManagers: LogManager[]) {}
|
||||
|
||||
/**
|
||||
* Logs a message to all logManagers
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param message Message to log
|
||||
* @param showTimestamp Whether to show the timestamp in the log
|
||||
*/
|
||||
async log(
|
||||
level: LogLevel,
|
||||
entity: string,
|
||||
message: string,
|
||||
showTimestamp = true
|
||||
) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.log(level, entity, message, showTimestamp);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Logs a message to all logManagers
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param message Message to log
|
||||
* @param showTimestamp Whether to show the timestamp in the log
|
||||
*/
|
||||
async log(
|
||||
level: LogLevel,
|
||||
entity: string,
|
||||
message: string,
|
||||
showTimestamp = true,
|
||||
) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.log(level, entity, message, showTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error to all logManagers
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param error Error to log
|
||||
*/
|
||||
async logError(level: LogLevel, entity: string, error: Error) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.logError(level, entity, error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Logs an error to all logManagers
|
||||
* @param level Importance of the log
|
||||
* @param entity Emitter of the log
|
||||
* @param error Error to log
|
||||
*/
|
||||
async logError(level: LogLevel, entity: string, error: Error) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.logError(level, entity, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a request to all logManagers
|
||||
* @param req Request to log
|
||||
* @param ip IP of the request
|
||||
* @param logAllDetails Whether to log all details of the request
|
||||
*/
|
||||
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.logRequest(req, ip, logAllDetails);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Logs a request to all logManagers
|
||||
* @param req Request to log
|
||||
* @param ip IP of the request
|
||||
* @param logAllDetails Whether to log all details of the request
|
||||
*/
|
||||
async logRequest(req: Request, ip?: string, logAllDetails = false) {
|
||||
for (const logManager of this.logManagers) {
|
||||
await logManager.logRequest(req, ip, logAllDetails);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MultiLogManager from multiple LogManager instances
|
||||
* @param logManagers LogManager instances to use
|
||||
* @returns
|
||||
*/
|
||||
static fromLogManagers(...logManagers: LogManager[]) {
|
||||
return new MultiLogManager(logManagers);
|
||||
}
|
||||
/**
|
||||
* Create a MultiLogManager from multiple LogManager instances
|
||||
* @param logManagers LogManager instances to use
|
||||
* @returns
|
||||
*/
|
||||
static fromLogManagers(...logManagers: LogManager[]) {
|
||||
return new MultiLogManager(logManagers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
"name": "log-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { }
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,117 +1,117 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts
|
||||
import { LogManager, LogLevel, MultiLogManager } from "../index";
|
||||
import type fs from "fs/promises";
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
beforeEach,
|
||||
expect,
|
||||
jest,
|
||||
mock,
|
||||
type Mock,
|
||||
test,
|
||||
type Mock,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
jest,
|
||||
mock,
|
||||
test,
|
||||
} from "bun:test";
|
||||
import type fs from "node:fs/promises";
|
||||
import type { BunFile } from "bun";
|
||||
// FILEPATH: /home/jessew/Dev/lysand/packages/log-manager/log-manager.test.ts
|
||||
import { LogLevel, LogManager, MultiLogManager } from "../index";
|
||||
|
||||
describe("LogManager", () => {
|
||||
let logManager: LogManager;
|
||||
let mockOutput: BunFile;
|
||||
let mockAppend: Mock<typeof fs.appendFile>;
|
||||
let logManager: LogManager;
|
||||
let mockOutput: BunFile;
|
||||
let mockAppend: Mock<typeof fs.appendFile>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockOutput = Bun.file("test.log");
|
||||
mockAppend = jest.fn();
|
||||
await mock.module("fs/promises", () => ({
|
||||
appendFile: mockAppend,
|
||||
}));
|
||||
logManager = new LogManager(mockOutput);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
mockOutput = Bun.file("test.log");
|
||||
mockAppend = jest.fn();
|
||||
await mock.module("fs/promises", () => ({
|
||||
appendFile: mockAppend,
|
||||
}));
|
||||
logManager = new LogManager(mockOutput);
|
||||
});
|
||||
|
||||
it("should initialize and write init log", () => {
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("--- INIT LogManager at")
|
||||
);
|
||||
});
|
||||
it("should initialize and write init log", () => {
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("--- INIT LogManager at"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log message with timestamp", async () => {
|
||||
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("[INFO] TestEntity: Test message")
|
||||
);
|
||||
});
|
||||
it("should log message with timestamp", async () => {
|
||||
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("[INFO] TestEntity: Test message"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log message without timestamp", async () => {
|
||||
await logManager.log(
|
||||
LogLevel.INFO,
|
||||
"TestEntity",
|
||||
"Test message",
|
||||
false
|
||||
);
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
"[INFO] TestEntity: Test message\n"
|
||||
);
|
||||
});
|
||||
it("should log message without timestamp", async () => {
|
||||
await logManager.log(
|
||||
LogLevel.INFO,
|
||||
"TestEntity",
|
||||
"Test message",
|
||||
false,
|
||||
);
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
"[INFO] TestEntity: Test message\n",
|
||||
);
|
||||
});
|
||||
|
||||
test.skip("should write to stdout", async () => {
|
||||
logManager = new LogManager(Bun.stdout);
|
||||
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||
test.skip("should write to stdout", async () => {
|
||||
logManager = new LogManager(Bun.stdout);
|
||||
await logManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||
|
||||
const writeMock = jest.fn();
|
||||
const writeMock = jest.fn();
|
||||
|
||||
await mock.module("Bun", () => ({
|
||||
stdout: Bun.stdout,
|
||||
write: writeMock,
|
||||
}));
|
||||
await mock.module("Bun", () => ({
|
||||
stdout: Bun.stdout,
|
||||
write: writeMock,
|
||||
}));
|
||||
|
||||
expect(writeMock).toHaveBeenCalledWith(
|
||||
Bun.stdout,
|
||||
expect.stringContaining("[INFO] TestEntity: Test message")
|
||||
);
|
||||
});
|
||||
expect(writeMock).toHaveBeenCalledWith(
|
||||
Bun.stdout,
|
||||
expect.stringContaining("[INFO] TestEntity: Test message"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error if output file does not exist", () => {
|
||||
mockAppend.mockImplementationOnce(() => {
|
||||
return Promise.reject(
|
||||
new Error("Output file doesnt exist (and isnt stdout)")
|
||||
);
|
||||
});
|
||||
expect(
|
||||
logManager.log(LogLevel.INFO, "TestEntity", "Test message")
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
it("should throw error if output file does not exist", () => {
|
||||
mockAppend.mockImplementationOnce(() => {
|
||||
return Promise.reject(
|
||||
new Error("Output file doesnt exist (and isnt stdout)"),
|
||||
);
|
||||
});
|
||||
expect(
|
||||
logManager.log(LogLevel.INFO, "TestEntity", "Test message"),
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
it("should log error message", async () => {
|
||||
const error = new Error("Test error");
|
||||
await logManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("[ERROR] TestEntity: Test error")
|
||||
);
|
||||
});
|
||||
it("should log error message", async () => {
|
||||
const error = new Error("Test error");
|
||||
await logManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("[ERROR] TestEntity: Test error"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log basic request details", async () => {
|
||||
const req = new Request("http://localhost/test", { method: "GET" });
|
||||
await logManager.logRequest(req, "127.0.0.1");
|
||||
it("should log basic request details", async () => {
|
||||
const req = new Request("http://localhost/test", { method: "GET" });
|
||||
await logManager.logRequest(req, "127.0.0.1");
|
||||
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("127.0.0.1: GET http://localhost/test")
|
||||
);
|
||||
});
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining("127.0.0.1: GET http://localhost/test"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("Request logger", () => {
|
||||
it("should log all request details for JSON content type", async () => {
|
||||
const req = new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ test: "value" }),
|
||||
});
|
||||
await logManager.logRequest(req, "127.0.0.1", true);
|
||||
describe("Request logger", () => {
|
||||
it("should log all request details for JSON content type", async () => {
|
||||
const req = new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ test: "value" }),
|
||||
});
|
||||
await logManager.logRequest(req, "127.0.0.1", true);
|
||||
|
||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||
[Headers]
|
||||
content-type: application/json
|
||||
[Body]
|
||||
|
|
@ -120,112 +120,112 @@ describe("LogManager", () => {
|
|||
}
|
||||
`;
|
||||
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(expectedLog)
|
||||
);
|
||||
});
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(expectedLog),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log all request details for text content type", async () => {
|
||||
const req = new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
body: "Test body",
|
||||
});
|
||||
await logManager.logRequest(req, "127.0.0.1", true);
|
||||
it("should log all request details for text content type", async () => {
|
||||
const req = new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
body: "Test body",
|
||||
});
|
||||
await logManager.logRequest(req, "127.0.0.1", true);
|
||||
|
||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||
[Headers]
|
||||
content-type: text/plain
|
||||
[Body]
|
||||
Test body
|
||||
`;
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(expectedLog)
|
||||
);
|
||||
});
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(expectedLog),
|
||||
);
|
||||
});
|
||||
|
||||
it("should log all request details for FormData content-type", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("test", "value");
|
||||
const req = new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
await logManager.logRequest(req, "127.0.0.1", true);
|
||||
it("should log all request details for FormData content-type", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("test", "value");
|
||||
const req = new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
await logManager.logRequest(req, "127.0.0.1", true);
|
||||
|
||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||
const expectedLog = `127.0.0.1: POST http://localhost/test
|
||||
[Headers]
|
||||
content-type: multipart/form-data; boundary=${
|
||||
req.headers.get("Content-Type")?.split("boundary=")[1] ?? ""
|
||||
}
|
||||
req.headers.get("Content-Type")?.split("boundary=")[1] ?? ""
|
||||
}
|
||||
[Body]
|
||||
test: value
|
||||
`;
|
||||
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(
|
||||
expectedLog.replace("----", expect.any(String))
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(mockAppend).toHaveBeenCalledWith(
|
||||
mockOutput.name,
|
||||
expect.stringContaining(
|
||||
expectedLog.replace("----", expect.any(String)),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MultiLogManager", () => {
|
||||
let multiLogManager: MultiLogManager;
|
||||
let mockLogManagers: LogManager[];
|
||||
let mockLog: jest.Mock;
|
||||
let mockLogError: jest.Mock;
|
||||
let mockLogRequest: jest.Mock;
|
||||
let multiLogManager: MultiLogManager;
|
||||
let mockLogManagers: LogManager[];
|
||||
let mockLog: jest.Mock;
|
||||
let mockLogError: jest.Mock;
|
||||
let mockLogRequest: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLog = jest.fn();
|
||||
mockLogError = jest.fn();
|
||||
mockLogRequest = jest.fn();
|
||||
mockLogManagers = [
|
||||
{
|
||||
log: mockLog,
|
||||
logError: mockLogError,
|
||||
logRequest: mockLogRequest,
|
||||
},
|
||||
{
|
||||
log: mockLog,
|
||||
logError: mockLogError,
|
||||
logRequest: mockLogRequest,
|
||||
},
|
||||
] as unknown as LogManager[];
|
||||
multiLogManager = MultiLogManager.fromLogManagers(...mockLogManagers);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockLog = jest.fn();
|
||||
mockLogError = jest.fn();
|
||||
mockLogRequest = jest.fn();
|
||||
mockLogManagers = [
|
||||
{
|
||||
log: mockLog,
|
||||
logError: mockLogError,
|
||||
logRequest: mockLogRequest,
|
||||
},
|
||||
{
|
||||
log: mockLog,
|
||||
logError: mockLogError,
|
||||
logRequest: mockLogRequest,
|
||||
},
|
||||
] as unknown as LogManager[];
|
||||
multiLogManager = MultiLogManager.fromLogManagers(...mockLogManagers);
|
||||
});
|
||||
|
||||
it("should log message to all logManagers", async () => {
|
||||
await multiLogManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||
expect(mockLog).toHaveBeenCalledTimes(2);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
LogLevel.INFO,
|
||||
"TestEntity",
|
||||
"Test message",
|
||||
true
|
||||
);
|
||||
});
|
||||
it("should log message to all logManagers", async () => {
|
||||
await multiLogManager.log(LogLevel.INFO, "TestEntity", "Test message");
|
||||
expect(mockLog).toHaveBeenCalledTimes(2);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
LogLevel.INFO,
|
||||
"TestEntity",
|
||||
"Test message",
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should log error to all logManagers", async () => {
|
||||
const error = new Error("Test error");
|
||||
await multiLogManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||
expect(mockLogError).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
LogLevel.ERROR,
|
||||
"TestEntity",
|
||||
error
|
||||
);
|
||||
});
|
||||
it("should log error to all logManagers", async () => {
|
||||
const error = new Error("Test error");
|
||||
await multiLogManager.logError(LogLevel.ERROR, "TestEntity", error);
|
||||
expect(mockLogError).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
LogLevel.ERROR,
|
||||
"TestEntity",
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
it("should log request to all logManagers", async () => {
|
||||
const req = new Request("http://localhost/test", { method: "GET" });
|
||||
await multiLogManager.logRequest(req, "127.0.0.1", true);
|
||||
expect(mockLogRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogRequest).toHaveBeenCalledWith(req, "127.0.0.1", true);
|
||||
});
|
||||
it("should log request to all logManagers", async () => {
|
||||
const req = new Request("http://localhost/test", { method: "GET" });
|
||||
await multiLogManager.logRequest(req, "127.0.0.1", true);
|
||||
expect(mockLogRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogRequest).toHaveBeenCalledWith(req, "127.0.0.1", true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,64 +1,65 @@
|
|||
import type { Config } from "config-manager";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConvertableMediaFormats } from "../media-converter";
|
||||
import { MediaConverter } from "../media-converter";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConfigType } from "config-manager";
|
||||
|
||||
export class LocalMediaBackend extends MediaBackend {
|
||||
constructor(config: ConfigType) {
|
||||
super(config, MediaBackendType.LOCAL);
|
||||
}
|
||||
constructor(config: Config) {
|
||||
super(config, MediaBackendType.LOCAL);
|
||||
}
|
||||
|
||||
public async addFile(file: File) {
|
||||
if (this.shouldConvertImages(this.config)) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
const mediaConverter = new MediaConverter(
|
||||
fileExtension as ConvertableMediaFormats,
|
||||
this.config.media.conversion
|
||||
.convert_to as ConvertableMediaFormats
|
||||
);
|
||||
file = await mediaConverter.convert(file);
|
||||
}
|
||||
public async addFile(file: File) {
|
||||
let convertedFile = file;
|
||||
if (this.shouldConvertImages(this.config)) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
const mediaConverter = new MediaConverter(
|
||||
fileExtension as ConvertableMediaFormats,
|
||||
this.config.media.conversion
|
||||
.convert_to as ConvertableMediaFormats,
|
||||
);
|
||||
convertedFile = await mediaConverter.convert(file);
|
||||
}
|
||||
|
||||
const hash = await new MediaHasher().getMediaHash(file);
|
||||
const hash = await new MediaHasher().getMediaHash(convertedFile);
|
||||
|
||||
const newFile = Bun.file(
|
||||
`${this.config.media.local_uploads_folder}/${hash}`
|
||||
);
|
||||
const newFile = Bun.file(
|
||||
`${this.config.media.local_uploads_folder}/${hash}`,
|
||||
);
|
||||
|
||||
if (await newFile.exists()) {
|
||||
throw new Error("File already exists");
|
||||
}
|
||||
if (await newFile.exists()) {
|
||||
throw new Error("File already exists");
|
||||
}
|
||||
|
||||
await Bun.write(newFile, file);
|
||||
await Bun.write(newFile, convertedFile);
|
||||
|
||||
return {
|
||||
uploadedFile: file,
|
||||
path: `./uploads/${file.name}`,
|
||||
hash: hash,
|
||||
};
|
||||
}
|
||||
return {
|
||||
uploadedFile: convertedFile,
|
||||
path: `./uploads/${convertedFile.name}`,
|
||||
hash: hash,
|
||||
};
|
||||
}
|
||||
|
||||
public async getFileByHash(
|
||||
hash: string,
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>
|
||||
): Promise<File | null> {
|
||||
const filename = await databaseHashFetcher(hash);
|
||||
public async getFileByHash(
|
||||
hash: string,
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>,
|
||||
): Promise<File | null> {
|
||||
const filename = await databaseHashFetcher(hash);
|
||||
|
||||
if (!filename) return null;
|
||||
if (!filename) return null;
|
||||
|
||||
return this.getFile(filename);
|
||||
}
|
||||
return this.getFile(filename);
|
||||
}
|
||||
|
||||
public async getFile(filename: string): Promise<File | null> {
|
||||
const file = Bun.file(
|
||||
`${this.config.media.local_uploads_folder}/${filename}`
|
||||
);
|
||||
public async getFile(filename: string): Promise<File | null> {
|
||||
const file = Bun.file(
|
||||
`${this.config.media.local_uploads_folder}/${filename}`,
|
||||
);
|
||||
|
||||
if (!(await file.exists())) return null;
|
||||
if (!(await file.exists())) return null;
|
||||
|
||||
return new File([await file.arrayBuffer()], filename, {
|
||||
type: file.type,
|
||||
lastModified: file.lastModified,
|
||||
});
|
||||
}
|
||||
return new File([await file.arrayBuffer()], filename, {
|
||||
type: file.type,
|
||||
lastModified: file.lastModified,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,74 @@
|
|||
import { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
|
||||
import type { Config } from "config-manager";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConvertableMediaFormats } from "../media-converter";
|
||||
import { MediaConverter } from "../media-converter";
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { ConfigType } from "config-manager";
|
||||
|
||||
export class S3MediaBackend extends MediaBackend {
|
||||
constructor(
|
||||
config: ConfigType,
|
||||
private s3Client = new S3Client({
|
||||
endPoint: config.s3.endpoint,
|
||||
useSSL: true,
|
||||
region: config.s3.region || "auto",
|
||||
bucket: config.s3.bucket_name,
|
||||
accessKey: config.s3.access_key,
|
||||
secretKey: config.s3.secret_access_key,
|
||||
})
|
||||
) {
|
||||
super(config, MediaBackendType.S3);
|
||||
}
|
||||
constructor(
|
||||
config: Config,
|
||||
private s3Client = new S3Client({
|
||||
endPoint: config.s3.endpoint,
|
||||
useSSL: true,
|
||||
region: config.s3.region || "auto",
|
||||
bucket: config.s3.bucket_name,
|
||||
accessKey: config.s3.access_key,
|
||||
secretKey: config.s3.secret_access_key,
|
||||
}),
|
||||
) {
|
||||
super(config, MediaBackendType.S3);
|
||||
}
|
||||
|
||||
public async addFile(file: File) {
|
||||
if (this.shouldConvertImages(this.config)) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
const mediaConverter = new MediaConverter(
|
||||
fileExtension as ConvertableMediaFormats,
|
||||
this.config.media.conversion
|
||||
.convert_to as ConvertableMediaFormats
|
||||
);
|
||||
file = await mediaConverter.convert(file);
|
||||
}
|
||||
public async addFile(file: File) {
|
||||
let convertedFile = file;
|
||||
if (this.shouldConvertImages(this.config)) {
|
||||
const fileExtension = file.name.split(".").pop();
|
||||
const mediaConverter = new MediaConverter(
|
||||
fileExtension as ConvertableMediaFormats,
|
||||
this.config.media.conversion
|
||||
.convert_to as ConvertableMediaFormats,
|
||||
);
|
||||
convertedFile = await mediaConverter.convert(file);
|
||||
}
|
||||
|
||||
const hash = await new MediaHasher().getMediaHash(file);
|
||||
const hash = await new MediaHasher().getMediaHash(convertedFile);
|
||||
|
||||
await this.s3Client.putObject(file.name, file.stream(), {
|
||||
size: file.size,
|
||||
});
|
||||
await this.s3Client.putObject(
|
||||
convertedFile.name,
|
||||
convertedFile.stream(),
|
||||
{
|
||||
size: convertedFile.size,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
uploadedFile: file,
|
||||
hash: hash,
|
||||
};
|
||||
}
|
||||
return {
|
||||
uploadedFile: convertedFile,
|
||||
hash: hash,
|
||||
};
|
||||
}
|
||||
|
||||
public async getFileByHash(
|
||||
hash: string,
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>
|
||||
): Promise<File | null> {
|
||||
const filename = await databaseHashFetcher(hash);
|
||||
public async getFileByHash(
|
||||
hash: string,
|
||||
databaseHashFetcher: (sha256: string) => Promise<string | null>,
|
||||
): Promise<File | null> {
|
||||
const filename = await databaseHashFetcher(hash);
|
||||
|
||||
if (!filename) return null;
|
||||
if (!filename) return null;
|
||||
|
||||
return this.getFile(filename);
|
||||
}
|
||||
return this.getFile(filename);
|
||||
}
|
||||
|
||||
public async getFile(filename: string): Promise<File | null> {
|
||||
try {
|
||||
await this.s3Client.statObject(filename);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
public async getFile(filename: string): Promise<File | null> {
|
||||
try {
|
||||
await this.s3Client.statObject(filename);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const file = await this.s3Client.getObject(filename);
|
||||
const file = await this.s3Client.getObject(filename);
|
||||
|
||||
return new File([await file.arrayBuffer()], filename, {
|
||||
type: file.headers.get("Content-Type") || "undefined",
|
||||
});
|
||||
}
|
||||
return new File([await file.arrayBuffer()], filename, {
|
||||
type: file.headers.get("Content-Type") || "undefined",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,101 +1,101 @@
|
|||
import type { ConfigType } from "config-manager";
|
||||
import type { Config } from "config-manager";
|
||||
|
||||
export enum MediaBackendType {
|
||||
LOCAL = "local",
|
||||
S3 = "s3",
|
||||
LOCAL = "local",
|
||||
S3 = "s3",
|
||||
}
|
||||
|
||||
interface UploadedFileMetadata {
|
||||
uploadedFile: File;
|
||||
path?: string;
|
||||
hash: string;
|
||||
uploadedFile: File;
|
||||
path?: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export class MediaHasher {
|
||||
/**
|
||||
* Returns the SHA-256 hash of a file in hex format
|
||||
* @param media The file to hash
|
||||
* @returns The SHA-256 hash of the file in hex format
|
||||
*/
|
||||
public async getMediaHash(media: File) {
|
||||
const hash = new Bun.SHA256()
|
||||
.update(await media.arrayBuffer())
|
||||
.digest("hex");
|
||||
/**
|
||||
* Returns the SHA-256 hash of a file in hex format
|
||||
* @param media The file to hash
|
||||
* @returns The SHA-256 hash of the file in hex format
|
||||
*/
|
||||
public async getMediaHash(media: File) {
|
||||
const hash = new Bun.SHA256()
|
||||
.update(await media.arrayBuffer())
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaBackend {
|
||||
constructor(
|
||||
public config: ConfigType,
|
||||
public backend: MediaBackendType
|
||||
) {}
|
||||
constructor(
|
||||
public config: Config,
|
||||
public backend: MediaBackendType,
|
||||
) {}
|
||||
|
||||
static async fromBackendType(
|
||||
backend: MediaBackendType,
|
||||
config: ConfigType
|
||||
): Promise<MediaBackend> {
|
||||
switch (backend) {
|
||||
case MediaBackendType.LOCAL:
|
||||
return new (await import("./backends/local")).LocalMediaBackend(
|
||||
config
|
||||
);
|
||||
case MediaBackendType.S3:
|
||||
return new (await import("./backends/s3")).S3MediaBackend(
|
||||
config
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown backend type: ${backend as any}`);
|
||||
}
|
||||
}
|
||||
static async fromBackendType(
|
||||
backend: MediaBackendType,
|
||||
config: Config,
|
||||
): Promise<MediaBackend> {
|
||||
switch (backend) {
|
||||
case MediaBackendType.LOCAL:
|
||||
return new (await import("./backends/local")).LocalMediaBackend(
|
||||
config,
|
||||
);
|
||||
case MediaBackendType.S3:
|
||||
return new (await import("./backends/s3")).S3MediaBackend(
|
||||
config,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown backend type: ${backend as string}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getBackendType() {
|
||||
return this.backend;
|
||||
}
|
||||
public getBackendType() {
|
||||
return this.backend;
|
||||
}
|
||||
|
||||
public shouldConvertImages(config: ConfigType) {
|
||||
return config.media.conversion.convert_images;
|
||||
}
|
||||
public shouldConvertImages(config: Config) {
|
||||
return config.media.conversion.convert_images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches file from backend from SHA-256 hash
|
||||
* @param file SHA-256 hash of wanted file
|
||||
* @param databaseHashFetcher Function that takes in a sha256 hash as input and outputs the filename of that file in the database
|
||||
* @returns The file as a File object
|
||||
*/
|
||||
public getFileByHash(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
file: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
databaseHashFetcher: (sha256: string) => Promise<string>
|
||||
): Promise<File | null> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass")
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Fetches file from backend from SHA-256 hash
|
||||
* @param file SHA-256 hash of wanted file
|
||||
* @param databaseHashFetcher Function that takes in a sha256 hash as input and outputs the filename of that file in the database
|
||||
* @returns The file as a File object
|
||||
*/
|
||||
public getFileByHash(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
file: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
databaseHashFetcher: (sha256: string) => Promise<string>,
|
||||
): Promise<File | null> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches file from backend from filename
|
||||
* @param filename File name
|
||||
* @returns The file as a File object
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public getFile(filename: string): Promise<File | null> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass")
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Fetches file from backend from filename
|
||||
* @param filename File name
|
||||
* @returns The file as a File object
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public getFile(filename: string): Promise<File | null> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds file to backend
|
||||
* @param file File to add
|
||||
* @returns Metadata about the uploaded file
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public addFile(file: File): Promise<UploadedFileMetadata> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass")
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Adds file to backend
|
||||
* @param file File to add
|
||||
* @returns Metadata about the uploaded file
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public addFile(file: File): Promise<UploadedFileMetadata> {
|
||||
return Promise.reject(
|
||||
new Error("Do not call MediaBackend directly: use a subclass"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,89 +6,89 @@
|
|||
import sharp from "sharp";
|
||||
|
||||
export enum ConvertableMediaFormats {
|
||||
PNG = "png",
|
||||
WEBP = "webp",
|
||||
JPEG = "jpeg",
|
||||
JPG = "jpg",
|
||||
AVIF = "avif",
|
||||
JXL = "jxl",
|
||||
HEIF = "heif",
|
||||
PNG = "png",
|
||||
WEBP = "webp",
|
||||
JPEG = "jpeg",
|
||||
JPG = "jpg",
|
||||
AVIF = "avif",
|
||||
JXL = "jxl",
|
||||
HEIF = "heif",
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles media conversion between formats
|
||||
*/
|
||||
export class MediaConverter {
|
||||
constructor(
|
||||
public fromFormat: ConvertableMediaFormats,
|
||||
public toFormat: ConvertableMediaFormats
|
||||
) {}
|
||||
constructor(
|
||||
public fromFormat: ConvertableMediaFormats,
|
||||
public toFormat: ConvertableMediaFormats,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns whether the media is convertable
|
||||
* @returns Whether the media is convertable
|
||||
*/
|
||||
public isConvertable() {
|
||||
return (
|
||||
this.fromFormat !== this.toFormat &&
|
||||
Object.values(ConvertableMediaFormats).includes(this.fromFormat)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns whether the media is convertable
|
||||
* @returns Whether the media is convertable
|
||||
*/
|
||||
public isConvertable() {
|
||||
return (
|
||||
this.fromFormat !== this.toFormat &&
|
||||
Object.values(ConvertableMediaFormats).includes(this.fromFormat)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file name with the extension replaced
|
||||
* @param fileName File name to replace
|
||||
* @returns File name with extension replaced
|
||||
*/
|
||||
private getReplacedFileName(fileName: string) {
|
||||
return this.extractFilenameFromPath(fileName).replace(
|
||||
new RegExp(`\\.${this.fromFormat}$`),
|
||||
`.${this.toFormat}`
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns the file name with the extension replaced
|
||||
* @param fileName File name to replace
|
||||
* @returns File name with extension replaced
|
||||
*/
|
||||
private getReplacedFileName(fileName: string) {
|
||||
return this.extractFilenameFromPath(fileName).replace(
|
||||
new RegExp(`\\.${this.fromFormat}$`),
|
||||
`.${this.toFormat}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the filename from a path
|
||||
* @param path Path to extract filename from
|
||||
* @returns Extracted filename
|
||||
*/
|
||||
private extractFilenameFromPath(path: string) {
|
||||
// Don't count escaped slashes as path separators
|
||||
const pathParts = path.split(/(?<!\\)\//);
|
||||
return pathParts[pathParts.length - 1];
|
||||
}
|
||||
/**
|
||||
* Extracts the filename from a path
|
||||
* @param path Path to extract filename from
|
||||
* @returns Extracted filename
|
||||
*/
|
||||
private extractFilenameFromPath(path: string) {
|
||||
// Don't count escaped slashes as path separators
|
||||
const pathParts = path.split(/(?<!\\)\//);
|
||||
return pathParts[pathParts.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts media to the specified format
|
||||
* @param media Media to convert
|
||||
* @returns Converted media
|
||||
*/
|
||||
public async convert(media: File) {
|
||||
if (!this.isConvertable()) {
|
||||
return media;
|
||||
}
|
||||
/**
|
||||
* Converts media to the specified format
|
||||
* @param media Media to convert
|
||||
* @returns Converted media
|
||||
*/
|
||||
public async convert(media: File) {
|
||||
if (!this.isConvertable()) {
|
||||
return media;
|
||||
}
|
||||
|
||||
const sharpCommand = sharp(await media.arrayBuffer());
|
||||
const sharpCommand = sharp(await media.arrayBuffer());
|
||||
|
||||
// Calculate newFilename before changing formats to prevent errors with jpg files
|
||||
const newFilename = this.getReplacedFileName(media.name);
|
||||
// Calculate newFilename before changing formats to prevent errors with jpg files
|
||||
const newFilename = this.getReplacedFileName(media.name);
|
||||
|
||||
if (this.fromFormat === ConvertableMediaFormats.JPG) {
|
||||
this.fromFormat = ConvertableMediaFormats.JPEG;
|
||||
}
|
||||
if (this.fromFormat === ConvertableMediaFormats.JPG) {
|
||||
this.fromFormat = ConvertableMediaFormats.JPEG;
|
||||
}
|
||||
|
||||
if (this.toFormat === ConvertableMediaFormats.JPG) {
|
||||
this.toFormat = ConvertableMediaFormats.JPEG;
|
||||
}
|
||||
if (this.toFormat === ConvertableMediaFormats.JPG) {
|
||||
this.toFormat = ConvertableMediaFormats.JPEG;
|
||||
}
|
||||
|
||||
const convertedBuffer = await sharpCommand[this.toFormat]().toBuffer();
|
||||
const convertedBuffer = await sharpCommand[this.toFormat]().toBuffer();
|
||||
|
||||
// Convert the buffer to a BlobPart
|
||||
const buffer = new Blob([convertedBuffer]);
|
||||
// Convert the buffer to a BlobPart
|
||||
const buffer = new Blob([convertedBuffer]);
|
||||
|
||||
return new File([buffer], newFilename, {
|
||||
type: `image/${this.toFormat}`,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
}
|
||||
return new File([buffer], newFilename, {
|
||||
type: `image/${this.toFormat}`,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "media-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"@jsr/bradenmacdonald__s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client",
|
||||
"config-manager": "workspace:*"
|
||||
}
|
||||
}
|
||||
"name": "media-manager",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"@jsr/bradenmacdonald__s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client",
|
||||
"config-manager": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,276 +1,277 @@
|
|||
import { beforeEach, describe, expect, it, jest, spyOn } from "bun:test";
|
||||
import type { S3Client } from "@jsr/bradenmacdonald__s3-lite-client";
|
||||
import type { Config } from "config-manager";
|
||||
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/backends/s3.test.ts
|
||||
import { MediaBackend, MediaBackendType, MediaHasher } from "..";
|
||||
import type { S3Client } from "@bradenmacdonald/s3-lite-client";
|
||||
import { beforeEach, describe, jest, it, expect, spyOn } from "bun:test";
|
||||
import { S3MediaBackend } from "../backends/s3";
|
||||
import type { ConfigType } from "config-manager";
|
||||
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||
import { LocalMediaBackend } from "../backends/local";
|
||||
import { S3MediaBackend } from "../backends/s3";
|
||||
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
|
||||
describe("MediaBackend", () => {
|
||||
let mediaBackend: MediaBackend;
|
||||
let mockConfig: ConfigType;
|
||||
let mediaBackend: MediaBackend;
|
||||
let mockConfig: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
media: {
|
||||
conversion: {
|
||||
convert_images: true,
|
||||
},
|
||||
},
|
||||
} as ConfigType;
|
||||
mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
media: {
|
||||
conversion: {
|
||||
convert_images: true,
|
||||
},
|
||||
},
|
||||
} as Config;
|
||||
mediaBackend = new MediaBackend(mockConfig, MediaBackendType.S3);
|
||||
});
|
||||
|
||||
it("should initialize with correct backend type", () => {
|
||||
expect(mediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
||||
});
|
||||
it("should initialize with correct backend type", () => {
|
||||
expect(mediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
||||
});
|
||||
|
||||
describe("fromBackendType", () => {
|
||||
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
|
||||
const backend = await MediaBackend.fromBackendType(
|
||||
MediaBackendType.LOCAL,
|
||||
mockConfig
|
||||
);
|
||||
expect(backend).toBeInstanceOf(LocalMediaBackend);
|
||||
});
|
||||
describe("fromBackendType", () => {
|
||||
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
|
||||
const backend = await MediaBackend.fromBackendType(
|
||||
MediaBackendType.LOCAL,
|
||||
mockConfig,
|
||||
);
|
||||
expect(backend).toBeInstanceOf(LocalMediaBackend);
|
||||
});
|
||||
|
||||
it("should return a S3MediaBackend instance for S3 backend type", async () => {
|
||||
const backend = await MediaBackend.fromBackendType(
|
||||
MediaBackendType.S3,
|
||||
{
|
||||
s3: {
|
||||
endpoint: "localhost:4566",
|
||||
region: "us-east-1",
|
||||
bucket_name: "test-bucket",
|
||||
access_key: "test-access",
|
||||
public_url: "test",
|
||||
secret_access_key: "test-secret",
|
||||
},
|
||||
} as ConfigType
|
||||
);
|
||||
expect(backend).toBeInstanceOf(S3MediaBackend);
|
||||
});
|
||||
it("should return a S3MediaBackend instance for S3 backend type", async () => {
|
||||
const backend = await MediaBackend.fromBackendType(
|
||||
MediaBackendType.S3,
|
||||
{
|
||||
s3: {
|
||||
endpoint: "localhost:4566",
|
||||
region: "us-east-1",
|
||||
bucket_name: "test-bucket",
|
||||
access_key: "test-access",
|
||||
public_url: "test",
|
||||
secret_access_key: "test-secret",
|
||||
},
|
||||
} as Config,
|
||||
);
|
||||
expect(backend).toBeInstanceOf(S3MediaBackend);
|
||||
});
|
||||
|
||||
it("should throw an error for unknown backend type", () => {
|
||||
expect(
|
||||
MediaBackend.fromBackendType("unknown" as any, mockConfig)
|
||||
).rejects.toThrow("Unknown backend type: unknown");
|
||||
});
|
||||
});
|
||||
it("should throw an error for unknown backend type", () => {
|
||||
expect(
|
||||
// @ts-expect-error This is a test
|
||||
MediaBackend.fromBackendType("unknown", mockConfig),
|
||||
).rejects.toThrow("Unknown backend type: unknown");
|
||||
});
|
||||
});
|
||||
|
||||
it("should check if images should be converted", () => {
|
||||
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(true);
|
||||
mockConfig.media.conversion.convert_images = false;
|
||||
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(false);
|
||||
});
|
||||
it("should check if images should be converted", () => {
|
||||
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(true);
|
||||
mockConfig.media.conversion.convert_images = false;
|
||||
expect(mediaBackend.shouldConvertImages(mockConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it("should throw error when calling getFileByHash", () => {
|
||||
const mockHash = "test-hash";
|
||||
const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg");
|
||||
it("should throw error when calling getFileByHash", () => {
|
||||
const mockHash = "test-hash";
|
||||
const databaseHashFetcher = jest.fn().mockResolvedValue("test.jpg");
|
||||
|
||||
expect(
|
||||
mediaBackend.getFileByHash(mockHash, databaseHashFetcher)
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
expect(
|
||||
mediaBackend.getFileByHash(mockHash, databaseHashFetcher),
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
it("should throw error when calling getFile", () => {
|
||||
const mockFilename = "test.jpg";
|
||||
it("should throw error when calling getFile", () => {
|
||||
const mockFilename = "test.jpg";
|
||||
|
||||
expect(mediaBackend.getFile(mockFilename)).rejects.toThrow(Error);
|
||||
});
|
||||
expect(mediaBackend.getFile(mockFilename)).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
it("should throw error when calling addFile", () => {
|
||||
const mockFile = new File([""], "test.jpg");
|
||||
it("should throw error when calling addFile", () => {
|
||||
const mockFile = new File([""], "test.jpg");
|
||||
|
||||
expect(mediaBackend.addFile(mockFile)).rejects.toThrow();
|
||||
});
|
||||
expect(mediaBackend.addFile(mockFile)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("S3MediaBackend", () => {
|
||||
let s3MediaBackend: S3MediaBackend;
|
||||
let mockS3Client: Partial<S3Client>;
|
||||
let mockConfig: DeepPartial<ConfigType>;
|
||||
let mockFile: File;
|
||||
let mockMediaHasher: MediaHasher;
|
||||
let s3MediaBackend: S3MediaBackend;
|
||||
let mockS3Client: Partial<S3Client>;
|
||||
let mockConfig: DeepPartial<Config>;
|
||||
let mockFile: File;
|
||||
let mockMediaHasher: MediaHasher;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
s3: {
|
||||
endpoint: "http://localhost:4566",
|
||||
region: "us-east-1",
|
||||
bucket_name: "test-bucket",
|
||||
access_key: "test-access-key",
|
||||
secret_access_key: "test-secret-access-key",
|
||||
public_url: "test",
|
||||
},
|
||||
media: {
|
||||
conversion: {
|
||||
convert_to: ConvertableMediaFormats.PNG,
|
||||
},
|
||||
},
|
||||
};
|
||||
mockFile = new File([new TextEncoder().encode("test")], "test.jpg");
|
||||
mockMediaHasher = new MediaHasher();
|
||||
mockS3Client = {
|
||||
putObject: jest.fn().mockResolvedValue({}),
|
||||
statObject: jest.fn().mockResolvedValue({}),
|
||||
getObject: jest.fn().mockResolvedValue({
|
||||
blob: jest.fn().mockResolvedValue(new Blob()),
|
||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||
}),
|
||||
} as Partial<S3Client>;
|
||||
s3MediaBackend = new S3MediaBackend(
|
||||
mockConfig as ConfigType,
|
||||
mockS3Client as S3Client
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
s3: {
|
||||
endpoint: "http://localhost:4566",
|
||||
region: "us-east-1",
|
||||
bucket_name: "test-bucket",
|
||||
access_key: "test-access-key",
|
||||
secret_access_key: "test-secret-access-key",
|
||||
public_url: "test",
|
||||
},
|
||||
media: {
|
||||
conversion: {
|
||||
convert_to: ConvertableMediaFormats.PNG,
|
||||
},
|
||||
},
|
||||
};
|
||||
mockFile = new File([new TextEncoder().encode("test")], "test.jpg");
|
||||
mockMediaHasher = new MediaHasher();
|
||||
mockS3Client = {
|
||||
putObject: jest.fn().mockResolvedValue({}),
|
||||
statObject: jest.fn().mockResolvedValue({}),
|
||||
getObject: jest.fn().mockResolvedValue({
|
||||
blob: jest.fn().mockResolvedValue(new Blob()),
|
||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||
}),
|
||||
} as Partial<S3Client>;
|
||||
s3MediaBackend = new S3MediaBackend(
|
||||
mockConfig as Config,
|
||||
mockS3Client as S3Client,
|
||||
);
|
||||
});
|
||||
|
||||
it("should initialize with correct type", () => {
|
||||
expect(s3MediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
||||
});
|
||||
it("should initialize with correct type", () => {
|
||||
expect(s3MediaBackend.getBackendType()).toEqual(MediaBackendType.S3);
|
||||
});
|
||||
|
||||
it("should add file", async () => {
|
||||
const mockHash = "test-hash";
|
||||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||
it("should add file", async () => {
|
||||
const mockHash = "test-hash";
|
||||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||
|
||||
const result = await s3MediaBackend.addFile(mockFile);
|
||||
const result = await s3MediaBackend.addFile(mockFile);
|
||||
|
||||
expect(result.uploadedFile).toEqual(mockFile);
|
||||
expect(result.hash).toHaveLength(64);
|
||||
expect(mockS3Client.putObject).toHaveBeenCalledWith(
|
||||
mockFile.name,
|
||||
expect.any(ReadableStream),
|
||||
{ size: mockFile.size }
|
||||
);
|
||||
});
|
||||
expect(result.uploadedFile).toEqual(mockFile);
|
||||
expect(result.hash).toHaveLength(64);
|
||||
expect(mockS3Client.putObject).toHaveBeenCalledWith(
|
||||
mockFile.name,
|
||||
expect.any(ReadableStream),
|
||||
{ size: mockFile.size },
|
||||
);
|
||||
});
|
||||
|
||||
it("should get file by hash", async () => {
|
||||
const mockHash = "test-hash";
|
||||
const mockFilename = "test.jpg";
|
||||
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
||||
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
||||
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||
});
|
||||
it("should get file by hash", async () => {
|
||||
const mockHash = "test-hash";
|
||||
const mockFilename = "test.jpg";
|
||||
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
||||
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
||||
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||
});
|
||||
|
||||
const file = await s3MediaBackend.getFileByHash(
|
||||
mockHash,
|
||||
databaseHashFetcher
|
||||
);
|
||||
const file = await s3MediaBackend.getFileByHash(
|
||||
mockHash,
|
||||
databaseHashFetcher,
|
||||
);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
|
||||
it("should get file", async () => {
|
||||
const mockFilename = "test.jpg";
|
||||
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
||||
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||
});
|
||||
it("should get file", async () => {
|
||||
const mockFilename = "test.jpg";
|
||||
mockS3Client.statObject = jest.fn().mockResolvedValue({});
|
||||
mockS3Client.getObject = jest.fn().mockResolvedValue({
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
||||
headers: new Headers({ "Content-Type": "image/jpeg" }),
|
||||
});
|
||||
|
||||
const file = await s3MediaBackend.getFile(mockFilename);
|
||||
const file = await s3MediaBackend.getFile(mockFilename);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("LocalMediaBackend", () => {
|
||||
let localMediaBackend: LocalMediaBackend;
|
||||
let mockConfig: ConfigType;
|
||||
let mockFile: File;
|
||||
let mockMediaHasher: MediaHasher;
|
||||
let localMediaBackend: LocalMediaBackend;
|
||||
let mockConfig: Config;
|
||||
let mockFile: File;
|
||||
let mockMediaHasher: MediaHasher;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
media: {
|
||||
conversion: {
|
||||
convert_images: true,
|
||||
convert_to: ConvertableMediaFormats.PNG,
|
||||
},
|
||||
local_uploads_folder: "./uploads",
|
||||
},
|
||||
} as ConfigType;
|
||||
mockFile = Bun.file(__dirname + "/megamind.jpg") as unknown as File;
|
||||
mockMediaHasher = new MediaHasher();
|
||||
localMediaBackend = new LocalMediaBackend(mockConfig);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
media: {
|
||||
conversion: {
|
||||
convert_images: true,
|
||||
convert_to: ConvertableMediaFormats.PNG,
|
||||
},
|
||||
local_uploads_folder: "./uploads",
|
||||
},
|
||||
} as Config;
|
||||
mockFile = Bun.file(`${__dirname}/megamind.jpg`) as unknown as File;
|
||||
mockMediaHasher = new MediaHasher();
|
||||
localMediaBackend = new LocalMediaBackend(mockConfig);
|
||||
});
|
||||
|
||||
it("should initialize with correct type", () => {
|
||||
expect(localMediaBackend.getBackendType()).toEqual(
|
||||
MediaBackendType.LOCAL
|
||||
);
|
||||
});
|
||||
it("should initialize with correct type", () => {
|
||||
expect(localMediaBackend.getBackendType()).toEqual(
|
||||
MediaBackendType.LOCAL,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add file", async () => {
|
||||
const mockHash = "test-hash";
|
||||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||
const mockMediaConverter = new MediaConverter(
|
||||
ConvertableMediaFormats.JPG,
|
||||
ConvertableMediaFormats.PNG
|
||||
);
|
||||
spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile);
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () => Promise.resolve(false),
|
||||
}));
|
||||
spyOn(Bun, "write").mockImplementationOnce(() =>
|
||||
Promise.resolve(mockFile.size)
|
||||
);
|
||||
it("should add file", async () => {
|
||||
const mockHash = "test-hash";
|
||||
spyOn(mockMediaHasher, "getMediaHash").mockResolvedValue(mockHash);
|
||||
const mockMediaConverter = new MediaConverter(
|
||||
ConvertableMediaFormats.JPG,
|
||||
ConvertableMediaFormats.PNG,
|
||||
);
|
||||
spyOn(mockMediaConverter, "convert").mockResolvedValue(mockFile);
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () => Promise.resolve(false),
|
||||
}));
|
||||
spyOn(Bun, "write").mockImplementationOnce(() =>
|
||||
Promise.resolve(mockFile.size),
|
||||
);
|
||||
|
||||
const result = await localMediaBackend.addFile(mockFile);
|
||||
const result = await localMediaBackend.addFile(mockFile);
|
||||
|
||||
expect(result.uploadedFile).toEqual(mockFile);
|
||||
expect(result.path).toEqual(`./uploads/megamind.png`);
|
||||
expect(result.hash).toHaveLength(64);
|
||||
});
|
||||
expect(result.uploadedFile).toEqual(mockFile);
|
||||
expect(result.path).toEqual("./uploads/megamind.png");
|
||||
expect(result.hash).toHaveLength(64);
|
||||
});
|
||||
|
||||
it("should get file by hash", async () => {
|
||||
const mockHash = "test-hash";
|
||||
const mockFilename = "test.jpg";
|
||||
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () => Promise.resolve(true),
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||
type: "image/jpeg",
|
||||
lastModified: 123456789,
|
||||
}));
|
||||
it("should get file by hash", async () => {
|
||||
const mockHash = "test-hash";
|
||||
const mockFilename = "test.jpg";
|
||||
const databaseHashFetcher = jest.fn().mockResolvedValue(mockFilename);
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () => Promise.resolve(true),
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||
type: "image/jpeg",
|
||||
lastModified: 123456789,
|
||||
}));
|
||||
|
||||
const file = await localMediaBackend.getFileByHash(
|
||||
mockHash,
|
||||
databaseHashFetcher
|
||||
);
|
||||
const file = await localMediaBackend.getFileByHash(
|
||||
mockHash,
|
||||
databaseHashFetcher,
|
||||
);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
|
||||
it("should get file", async () => {
|
||||
const mockFilename = "test.jpg";
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () => Promise.resolve(true),
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||
type: "image/jpeg",
|
||||
lastModified: 123456789,
|
||||
}));
|
||||
it("should get file", async () => {
|
||||
const mockFilename = "test.jpg";
|
||||
// @ts-expect-error This is a mock
|
||||
spyOn(Bun, "file").mockImplementationOnce(() => ({
|
||||
exists: () => Promise.resolve(true),
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||
type: "image/jpeg",
|
||||
lastModified: 123456789,
|
||||
}));
|
||||
|
||||
const file = await localMediaBackend.getFile(mockFilename);
|
||||
const file = await localMediaBackend.getFile(mockFilename);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toEqual(mockFilename);
|
||||
expect(file?.type).toEqual("image/jpeg");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,65 +1,65 @@
|
|||
// FILEPATH: /home/jessew/Dev/lysand/packages/media-manager/media-converter.test.ts
|
||||
import { describe, it, expect, beforeEach } from "bun:test";
|
||||
import { MediaConverter, ConvertableMediaFormats } from "../media-converter";
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { ConvertableMediaFormats, MediaConverter } from "../media-converter";
|
||||
|
||||
describe("MediaConverter", () => {
|
||||
let mediaConverter: MediaConverter;
|
||||
let mediaConverter: MediaConverter;
|
||||
|
||||
beforeEach(() => {
|
||||
mediaConverter = new MediaConverter(
|
||||
ConvertableMediaFormats.JPG,
|
||||
ConvertableMediaFormats.PNG
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
mediaConverter = new MediaConverter(
|
||||
ConvertableMediaFormats.JPG,
|
||||
ConvertableMediaFormats.PNG,
|
||||
);
|
||||
});
|
||||
|
||||
it("should initialize with correct formats", () => {
|
||||
expect(mediaConverter.fromFormat).toEqual(ConvertableMediaFormats.JPG);
|
||||
expect(mediaConverter.toFormat).toEqual(ConvertableMediaFormats.PNG);
|
||||
});
|
||||
it("should initialize with correct formats", () => {
|
||||
expect(mediaConverter.fromFormat).toEqual(ConvertableMediaFormats.JPG);
|
||||
expect(mediaConverter.toFormat).toEqual(ConvertableMediaFormats.PNG);
|
||||
});
|
||||
|
||||
it("should check if media is convertable", () => {
|
||||
expect(mediaConverter.isConvertable()).toBe(true);
|
||||
mediaConverter.toFormat = ConvertableMediaFormats.JPG;
|
||||
expect(mediaConverter.isConvertable()).toBe(false);
|
||||
});
|
||||
it("should check if media is convertable", () => {
|
||||
expect(mediaConverter.isConvertable()).toBe(true);
|
||||
mediaConverter.toFormat = ConvertableMediaFormats.JPG;
|
||||
expect(mediaConverter.isConvertable()).toBe(false);
|
||||
});
|
||||
|
||||
it("should replace file name extension", () => {
|
||||
const fileName = "test.jpg";
|
||||
const expectedFileName = "test.png";
|
||||
// Written like this because it's a private function
|
||||
expect(mediaConverter["getReplacedFileName"](fileName)).toEqual(
|
||||
expectedFileName
|
||||
);
|
||||
});
|
||||
it("should replace file name extension", () => {
|
||||
const fileName = "test.jpg";
|
||||
const expectedFileName = "test.png";
|
||||
// Written like this because it's a private function
|
||||
expect(mediaConverter.getReplacedFileName(fileName)).toEqual(
|
||||
expectedFileName,
|
||||
);
|
||||
});
|
||||
|
||||
describe("Filename extractor", () => {
|
||||
it("should extract filename from path", () => {
|
||||
const path = "path/to/test.jpg";
|
||||
const expectedFileName = "test.jpg";
|
||||
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual(
|
||||
expectedFileName
|
||||
);
|
||||
});
|
||||
describe("Filename extractor", () => {
|
||||
it("should extract filename from path", () => {
|
||||
const path = "path/to/test.jpg";
|
||||
const expectedFileName = "test.jpg";
|
||||
expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
|
||||
expectedFileName,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle escaped slashes", () => {
|
||||
const path = "path/to/test\\/test.jpg";
|
||||
const expectedFileName = "test\\/test.jpg";
|
||||
expect(mediaConverter["extractFilenameFromPath"](path)).toEqual(
|
||||
expectedFileName
|
||||
);
|
||||
});
|
||||
});
|
||||
it("should handle escaped slashes", () => {
|
||||
const path = "path/to/test\\/test.jpg";
|
||||
const expectedFileName = "test\\/test.jpg";
|
||||
expect(mediaConverter.extractFilenameFromPath(path)).toEqual(
|
||||
expectedFileName,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should convert media", async () => {
|
||||
const file = Bun.file(__dirname + "/megamind.jpg");
|
||||
it("should convert media", async () => {
|
||||
const file = Bun.file(`${__dirname}/megamind.jpg`);
|
||||
|
||||
const convertedFile = await mediaConverter.convert(
|
||||
file as unknown as File
|
||||
);
|
||||
const convertedFile = await mediaConverter.convert(
|
||||
file as unknown as File,
|
||||
);
|
||||
|
||||
expect(convertedFile.name).toEqual("megamind.png");
|
||||
expect(convertedFile.type).toEqual(
|
||||
`image/${ConvertableMediaFormats.PNG}`
|
||||
);
|
||||
});
|
||||
expect(convertedFile.name).toEqual("megamind.png");
|
||||
expect(convertedFile.type).toEqual(
|
||||
`image/${ConvertableMediaFormats.PNG}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { APActor, APNote } from "activitypub-types";
|
|||
import { ActivityPubTranslator } from "./protocols/activitypub";
|
||||
|
||||
export enum SupportedProtocols {
|
||||
ACTIVITYPUB = "activitypub",
|
||||
ACTIVITYPUB = "activitypub",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -12,37 +12,40 @@ export enum SupportedProtocols {
|
|||
* This class is not meant to be instantiated directly, but rather for its children to be used.
|
||||
*/
|
||||
export class ProtocolTranslator {
|
||||
static auto(object: any) {
|
||||
const protocol = this.recognizeProtocol(object);
|
||||
switch (protocol) {
|
||||
case SupportedProtocols.ACTIVITYPUB:
|
||||
return new ActivityPubTranslator();
|
||||
default:
|
||||
throw new Error("Unknown protocol");
|
||||
}
|
||||
}
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
static auto(object: any) {
|
||||
const protocol = ProtocolTranslator.recognizeProtocol(object);
|
||||
switch (protocol) {
|
||||
case SupportedProtocols.ACTIVITYPUB:
|
||||
return new ActivityPubTranslator();
|
||||
default:
|
||||
throw new Error("Unknown protocol");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an ActivityPub actor to a Lysand user
|
||||
* @param data Raw JSON-LD data from an ActivityPub actor
|
||||
*/
|
||||
user(data: APActor) {
|
||||
//
|
||||
}
|
||||
/**
|
||||
* Translates an ActivityPub actor to a Lysand user
|
||||
* @param data Raw JSON-LD data from an ActivityPub actor
|
||||
*/
|
||||
user(data: APActor) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an ActivityPub note to a Lysand status
|
||||
* @param data Raw JSON-LD data from an ActivityPub note
|
||||
*/
|
||||
status(data: APNote) {
|
||||
//
|
||||
}
|
||||
/**
|
||||
* Translates an ActivityPub note to a Lysand status
|
||||
* @param data Raw JSON-LD data from an ActivityPub note
|
||||
*/
|
||||
status(data: APNote) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically recognizes the protocol of a given object
|
||||
*/
|
||||
private static recognizeProtocol(object: any) {
|
||||
// Temporary stub
|
||||
return SupportedProtocols.ACTIVITYPUB;
|
||||
}
|
||||
/**
|
||||
* Automatically recognizes the protocol of a given object
|
||||
*/
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
private static recognizeProtocol(object: any) {
|
||||
// Temporary stub
|
||||
return SupportedProtocols.ACTIVITYPUB;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "protocol-translator",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"activitypub-types": "^1.1.0"
|
||||
}
|
||||
}
|
||||
"name": "protocol-translator",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"activitypub-types": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { ProtocolTranslator } from "..";
|
||||
|
||||
export class ActivityPubTranslator extends ProtocolTranslator {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
user() {
|
||||
|
||||
}
|
||||
}
|
||||
user() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,158 +13,158 @@
|
|||
* @returns JavaScript object of type T
|
||||
*/
|
||||
export class RequestParser {
|
||||
constructor(public request: Request) {}
|
||||
constructor(public request: Request) {}
|
||||
|
||||
/**
|
||||
* Parse request body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
async toObject<T>() {
|
||||
try {
|
||||
switch (await this.determineContentType()) {
|
||||
case "application/json":
|
||||
return this.parseJson<T>();
|
||||
case "application/x-www-form-urlencoded":
|
||||
return this.parseFormUrlencoded<T>();
|
||||
case "multipart/form-data":
|
||||
return this.parseFormData<T>();
|
||||
default:
|
||||
return this.parseQuery<T>();
|
||||
}
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse request body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
async toObject<T>() {
|
||||
try {
|
||||
switch (await this.determineContentType()) {
|
||||
case "application/json":
|
||||
return this.parseJson<T>();
|
||||
case "application/x-www-form-urlencoded":
|
||||
return this.parseFormUrlencoded<T>();
|
||||
case "multipart/form-data":
|
||||
return this.parseFormData<T>();
|
||||
default:
|
||||
return this.parseQuery<T>();
|
||||
}
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine body content type
|
||||
* If there is no Content-Type header, automatically
|
||||
* guess content type. Cuts off after ";" character
|
||||
* @returns Content-Type header value, or empty string if there is no body
|
||||
* @throws Error if body is invalid
|
||||
* @private
|
||||
*/
|
||||
private async determineContentType() {
|
||||
if (this.request.headers.get("Content-Type")) {
|
||||
return (
|
||||
this.request.headers.get("Content-Type")?.split(";")[0] ?? ""
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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 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
|
||||
}
|
||||
// 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 (this.request.body) {
|
||||
throw new Error("Invalid body");
|
||||
}
|
||||
|
||||
// If there is no body, return query parameters
|
||||
return "";
|
||||
}
|
||||
// If there is no body, return query parameters
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse FormData body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormData<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
/**
|
||||
* Parse FormData body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormData<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
result[key as keyof T] = value as any;
|
||||
} else if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
} else if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
|
||||
(result[arrayKey] as any[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
/**
|
||||
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
||||
const formData = await this.request.formData();
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
(result[arrayKey] as FormDataEntryValue[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseJson<T>(): Promise<Partial<T>> {
|
||||
try {
|
||||
return (await this.request.json()) as T;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse JSON body into a JavaScript object
|
||||
* @returns JavaScript object of type T
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
*/
|
||||
private async parseJson<T>(): Promise<Partial<T>> {
|
||||
try {
|
||||
return (await this.request.json()) as T;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse query parameters into a JavaScript object
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
* @returns JavaScript object of type T
|
||||
*/
|
||||
private parseQuery<T>(): Partial<T> {
|
||||
const result: Partial<T> = {};
|
||||
const url = new URL(this.request.url);
|
||||
/**
|
||||
* Parse query parameters into a JavaScript object
|
||||
* @private
|
||||
* @throws Error if body is invalid
|
||||
* @returns JavaScript object of type T
|
||||
*/
|
||||
private parseQuery<T>(): Partial<T> {
|
||||
const result: Partial<T> = {};
|
||||
const url = new URL(this.request.url);
|
||||
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
if (key.endsWith("[]")) {
|
||||
const arrayKey = key.slice(0, -2) as keyof T;
|
||||
if (!result[arrayKey]) {
|
||||
result[arrayKey] = [] as T[keyof T];
|
||||
}
|
||||
(result[arrayKey] as string[]).push(value);
|
||||
} else {
|
||||
result[key as keyof T] = value as any;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
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 T[keyof T];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,158 +1,158 @@
|
|||
import { describe, it, expect, test } from "bun:test";
|
||||
import { describe, expect, it, 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" });
|
||||
});
|
||||
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 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"],
|
||||
});
|
||||
});
|
||||
});
|
||||
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 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({});
|
||||
});
|
||||
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" });
|
||||
});
|
||||
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 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 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("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" });
|
||||
});
|
||||
});
|
||||
test("URL Encoded", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "param1=value1¶m2=value2",
|
||||
});
|
||||
const result = await new RequestParser(request).toObject<{
|
||||
param1: string;
|
||||
param2: string;
|
||||
}>();
|
||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue