This commit is contained in:
259
backend/src/utils/misc.ts
Normal file
259
backend/src/utils/misc.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time";
|
||||
import { roundTo2 } from "@monkeytype/util/numbers";
|
||||
import uaparser from "ua-parser-js";
|
||||
import { MonkeyRequest } from "../api/types";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
//todo split this file into smaller util files (grouped by functionality)
|
||||
|
||||
export function identity(value: unknown): string {
|
||||
return Object.prototype.toString
|
||||
.call(value)
|
||||
.replace(/^\[object\s+([a-z]+)\]$/i, "$1")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function base64UrlEncode(data: string): string {
|
||||
return Buffer.from(data).toString("base64url");
|
||||
}
|
||||
|
||||
export function base64UrlDecode(data: string): string {
|
||||
return Buffer.from(data, "base64url").toString();
|
||||
}
|
||||
|
||||
type AgentLog = {
|
||||
ip: string;
|
||||
agent: string;
|
||||
device?: string;
|
||||
};
|
||||
|
||||
export function buildAgentLog(req: MonkeyRequest): AgentLog {
|
||||
const agent = uaparser(req.raw.headers["user-agent"]);
|
||||
|
||||
const agentLog: AgentLog = {
|
||||
ip:
|
||||
(req.raw.headers["cf-connecting-ip"] as string) ||
|
||||
(req.raw.headers["x-forwarded-for"] as string) ||
|
||||
(req.raw.ip as string) ||
|
||||
"255.255.255.255",
|
||||
agent: `${agent.os.name} ${agent.os.version} ${agent.browser.name} ${agent.browser.version}`,
|
||||
};
|
||||
|
||||
const {
|
||||
device: { vendor, model, type },
|
||||
} = agent;
|
||||
|
||||
agentLog.device = `${vendor ?? "unknown vendor"} ${
|
||||
model ?? "unknown model"
|
||||
} ${type ?? "unknown type"}`;
|
||||
|
||||
return agentLog;
|
||||
}
|
||||
|
||||
export function padNumbers(
|
||||
numbers: number[],
|
||||
maxLength: number,
|
||||
fillString: string,
|
||||
): string[] {
|
||||
return numbers.map((number) =>
|
||||
number.toString().padStart(maxLength, fillString),
|
||||
);
|
||||
}
|
||||
|
||||
export function matchesAPattern(text: string, pattern: string): boolean {
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
return regex.test(text);
|
||||
}
|
||||
|
||||
export function kogascore(wpm: number, acc: number, timestamp: number): number {
|
||||
// its safe to round after multiplying by 100 (99.99 * 100 rounded will be 9999 not 100)
|
||||
// rounding is necessary to protect against floating point errors
|
||||
const normalizedWpm = Math.round(wpm * 100);
|
||||
const normalizedAcc = Math.round(acc * 100);
|
||||
|
||||
const padAmount = 100000;
|
||||
const firstPart = (padAmount + normalizedWpm) * padAmount;
|
||||
const secondPart = (firstPart + normalizedAcc) * padAmount;
|
||||
|
||||
const currentDayTimeMilliseconds =
|
||||
timestamp - (timestamp % MILLISECONDS_IN_DAY);
|
||||
const todayMilliseconds = timestamp - currentDayTimeMilliseconds;
|
||||
|
||||
return (
|
||||
secondPart + Math.floor((MILLISECONDS_IN_DAY - todayMilliseconds) / 1000)
|
||||
);
|
||||
}
|
||||
|
||||
export function flattenObjectDeep(
|
||||
obj: Record<string, unknown>,
|
||||
prefix = "",
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
const keys = Object.keys(obj);
|
||||
|
||||
keys.forEach((key) => {
|
||||
const value = obj[key];
|
||||
|
||||
const newPrefix = prefix.length > 0 ? `${prefix}.${key}` : key;
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
const flattened = flattenObjectDeep(value as Record<string, unknown>);
|
||||
const flattenedKeys = Object.keys(flattened);
|
||||
|
||||
if (flattenedKeys.length === 0) {
|
||||
result[newPrefix] = value;
|
||||
}
|
||||
|
||||
flattenedKeys.forEach((flattenedKey) => {
|
||||
result[`${newPrefix}.${flattenedKey}`] = flattened[flattenedKey];
|
||||
});
|
||||
} else {
|
||||
result[newPrefix] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function sanitizeString(str: string | undefined): string | undefined {
|
||||
if (str === undefined || str === "") {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str
|
||||
.replace(/[\u0300-\u036F]/g, "")
|
||||
.trim()
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.replace(/\s{3,}/g, " ");
|
||||
}
|
||||
|
||||
const suffixes = ["th", "st", "nd", "rd"];
|
||||
|
||||
export function getOrdinalNumberString(number: number): string {
|
||||
const lastTwo = number % 100;
|
||||
const suffix =
|
||||
suffixes[(lastTwo - 20) % 10] ?? suffixes[lastTwo] ?? suffixes[0];
|
||||
return `${number}${suffix}`;
|
||||
}
|
||||
|
||||
type TimeUnit =
|
||||
| "second"
|
||||
| "minute"
|
||||
| "hour"
|
||||
| "day"
|
||||
| "week"
|
||||
| "month"
|
||||
| "year";
|
||||
|
||||
export const MINUTE_IN_SECONDS = 1 * 60;
|
||||
export const HOUR_IN_SECONDS = 1 * 60 * MINUTE_IN_SECONDS;
|
||||
export const DAY_IN_SECONDS = 1 * 24 * HOUR_IN_SECONDS;
|
||||
export const WEEK_IN_SECONDS = 1 * 7 * DAY_IN_SECONDS;
|
||||
export const MONTH_IN_SECONDS = 1 * 30.4167 * DAY_IN_SECONDS;
|
||||
export const YEAR_IN_SECONDS = 1 * 12 * MONTH_IN_SECONDS;
|
||||
|
||||
export function formatSeconds(
|
||||
seconds: number,
|
||||
): `${number} ${TimeUnit}${"s" | ""}` {
|
||||
let unit: TimeUnit;
|
||||
let secondsInUnit: number;
|
||||
|
||||
if (seconds < MINUTE_IN_SECONDS) {
|
||||
unit = "second";
|
||||
secondsInUnit = 1;
|
||||
} else if (seconds < HOUR_IN_SECONDS) {
|
||||
unit = "minute";
|
||||
secondsInUnit = MINUTE_IN_SECONDS;
|
||||
} else if (seconds < DAY_IN_SECONDS) {
|
||||
unit = "hour";
|
||||
secondsInUnit = HOUR_IN_SECONDS;
|
||||
} else if (seconds < WEEK_IN_SECONDS) {
|
||||
unit = "day";
|
||||
secondsInUnit = DAY_IN_SECONDS;
|
||||
} else if (seconds < YEAR_IN_SECONDS) {
|
||||
if (seconds < WEEK_IN_SECONDS * 4) {
|
||||
unit = "week";
|
||||
secondsInUnit = WEEK_IN_SECONDS;
|
||||
} else {
|
||||
unit = "month";
|
||||
secondsInUnit = MONTH_IN_SECONDS;
|
||||
}
|
||||
} else {
|
||||
unit = "year";
|
||||
secondsInUnit = YEAR_IN_SECONDS;
|
||||
}
|
||||
|
||||
const normalized = roundTo2(seconds / secondsInUnit);
|
||||
|
||||
return `${normalized} ${unit}${normalized > 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
export function isDevEnvironment(): boolean {
|
||||
return process.env["MODE"] === "dev";
|
||||
}
|
||||
|
||||
export function getFrontendUrl(): string {
|
||||
return isDevEnvironment()
|
||||
? "http://localhost:3000"
|
||||
: (process.env["FRONTEND_URL"] ?? "https://monkeytype.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* convert database object into api object
|
||||
* @param data database object with `_id: ObjectId`
|
||||
* @returns api object with `id: string`
|
||||
*/
|
||||
|
||||
export function replaceObjectId<T extends { _id: ObjectId }>(
|
||||
data: T,
|
||||
): T & { _id: string };
|
||||
export function replaceObjectId<T extends { _id: ObjectId }>(
|
||||
data: T | null,
|
||||
): (T & { _id: string }) | null;
|
||||
export function replaceObjectId<T extends { _id: ObjectId }>(
|
||||
data: T | null,
|
||||
): (T & { _id: string }) | null {
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
const result = {
|
||||
...data,
|
||||
_id: data._id.toString(),
|
||||
} as T & { _id: string };
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert database objects into api objects
|
||||
* @param data database objects with `_id: ObjectId`
|
||||
* @returns api objects with `id: string`
|
||||
*/
|
||||
export function replaceObjectIds<T extends { _id: ObjectId }>(
|
||||
data: T[],
|
||||
): (T & { _id: string })[] {
|
||||
if (data === undefined) return data;
|
||||
return data.map((it) => replaceObjectId(it));
|
||||
}
|
||||
export type WithObjectId<T extends { _id: string }> = Omit<T, "_id"> & {
|
||||
_id: ObjectId;
|
||||
};
|
||||
|
||||
export function omit<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
keys: K[],
|
||||
): Omit<T, K> {
|
||||
const result = { ...obj };
|
||||
for (const key of keys) {
|
||||
// oxlint-disable-next-line no-dynamic-delete
|
||||
delete result[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isPlainObject(value: unknown): boolean {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
Object.getPrototypeOf(value) === Object.prototype
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user