Skip to content

Instantly share code, notes, and snippets.

@navin-moorthy
Last active November 28, 2023 12:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save navin-moorthy/8918004d2b666a44c4f37d31c88d0ced to your computer and use it in GitHub Desktop.
Save navin-moorthy/8918004d2b666a44c4f37d31c88d0ced to your computer and use it in GitHub Desktop.
Node Utils
import { type NextFunction, type Request, type Response } from "express";
export type AsyncHandler = (
request: Request,
response: Response,
next: NextFunction,
) => Promise<void>;
/**
* Wraps an asynchronous route handler function with error handling middleware.
* @param {AsyncHandler} asyncFunction - The async route handler function to wrap.
* @returns A new function that handles errors thrown by the async function.
*/
export function asyncWrapper(asyncFunction: AsyncHandler) {
/**
* The new function that wraps the async route handler function.
* @param {Request} request - The request object.
* @param {Response} response - The response object.
* @param {NextFunction} next - The next middleware function.
*/
return function (request: Request, response: Response, next: NextFunction) {
// eslint-disable-next-line promise/prefer-await-to-then
asyncFunction(request, response, next).catch(next);
};
}
import fsSync from "node:fs";
import fs from "node:fs/promises";
import { throwError } from "./throwError.js";
/**
* Checks if a directory exists and is readable and writable. If it doesn't exist, it creates it. If it exists but is not readable or writable, it makes it both.
* @async
* @function
* @param {string} directoryPath - The path of the directory to check.
* @returns {Promise<void>} A promise that resolves to a void indicating whether the directory exists and is readable and writable.
*/
export async function ensureDirectoryAccess(
directoryPath: string,
): Promise<void> {
try {
await fs.access(
directoryPath,
// eslint-disable-next-line no-bitwise
fsSync.constants.F_OK | fsSync.constants.W_OK | fsSync.constants.R_OK,
);
} catch {
try {
await fs.mkdir(directoryPath, { recursive: true });
await fs.chmod(directoryPath, 0o777);
} catch (error) {
throwError(
error,
`${directoryPath} does not exists / writable / readable in ensureDirectoryAccess function`,
);
}
}
}
/* eslint-disable node/no-sync */
import fsSync from "node:fs";
import { throwError } from "./throwError.js";
/**
* Checks if a directory exists and is readable and writable. If it doesn't exist, it creates it. If it exists but is not readable or writable, it makes it both.
* @function
* @param {string} directoryPath - The path of the directory to check.
* @returns {void} A void indicating whether the directory exists and is readable and writable.
*/
export function ensureDirectoryAccessSync(directoryPath: string): void {
try {
fsSync.accessSync(
directoryPath,
// eslint-disable-next-line no-bitwise
fsSync.constants.F_OK | fsSync.constants.W_OK | fsSync.constants.R_OK,
);
} catch {
try {
fsSync.mkdirSync(directoryPath, { recursive: true });
fsSync.chmodSync(directoryPath, 0o777);
} catch (error) {
throwError(
error,
`${directoryPath} does not exists / writable / readable in ensureDirectoryAccess function`,
);
}
}
}
import fsSync from "node:fs";
import fs, { type FileHandle } from "node:fs/promises";
import { throwError } from "./throwError.js";
/**
* Ensures that the file at the given path exists, is writable and is readable.
* @async
* @param {string} filePath - The path to the file to check.
* @returns {Promise<void>} A promise that resolves to true if the file is accessible, false otherwise.
*/
export async function ensureFileAccess(filePath: string): Promise<void> {
try {
await fs.access(
filePath,
// eslint-disable-next-line no-bitwise
fsSync.constants.F_OK | fsSync.constants.W_OK | fsSync.constants.R_OK,
);
} catch {
let fileHandle: FileHandle | undefined;
try {
fileHandle = await fs.open(filePath, "w");
} catch (error) {
throwError(
error,
`${filePath} does not exists / writable / readable in ensureFileAccess function`,
);
} finally {
await fileHandle?.close();
}
}
}
import prettier from "prettier";
/**
* Resolves the Prettier options by finding and reading the Prettier configuration file.
*
* @async
* @returns {Promise<prettier.Options>} A promise that resolves with the Prettier options.
* @throws {Error} If the Prettier options cannot be resolved.
*/
async function resolvePrettierOptions(): Promise<prettier.Options> {
try {
const configFile = await prettier.resolveConfigFile();
if (!configFile) {
throw new Error("Could not resolve Prettier config file.");
}
const options = await prettier.resolveConfig(configFile);
if (!options) {
throw new Error("Could not resolve Prettier options.");
}
return options;
} catch (error: unknown) {
throw new Error("Could not resolve Prettier options.", {
cause: error,
});
}
}
const prettierOptions = await resolvePrettierOptions();
/**
* Formats JSON data with Prettier.
*
* @param {*} data - The JSON data to format.
* @returns {string} The formatted JSON data.
*/
function formatWithPrettier(data: unknown): string {
return prettier.format(JSON.stringify(data), {
...prettierOptions,
parser: "json",
});
}
// https://markoskon.com/creating-font-subsets/#minimal-english-subset
const unicodeRange = `U+0100-0130,U+0132-0151,U+0154-017F`
.replaceAll("U+", "")
.split(",")
.reduce((response, item) => {
if (/-/u.test(item)) {
const [first, last] = item
.split("-")
.map((index) => Number.parseInt(index, 16));
const numbers = [];
for (let index = first; index <= last; index++) {
numbers.push(String.fromCodePoint(index));
}
return [...response, { range: item, characters: numbers.join(" ") }];
} else {
return [
...response,
{
range: item,
characters: String.fromCodePoint(Number.parseInt(item, 16)),
},
];
}
}, []);
console.log(unicodeRange);
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Get the directory name of a file URL.
*
* @param {string} url - The file URL.
* @returns {string} The directory name.
*/
export function getDirname(url: string): string {
return path.dirname(fileURLToPath(url));
}
import got from "got";
import { throwErrorWithAdditionalMessage } from "./throwErrorWithAdditionalMessage.js";
type GotGetFetcherProps = {
searchParameters?: Record<string, string>;
url: string;
};
export const gotGetFetcher = async <T>(
props: GotGetFetcherProps,
): Promise<T> => {
try {
const { searchParameters = {}, url } = props;
const urlSearchParameters = new URLSearchParams();
for (const [key, value] of Object.entries(searchParameters)) {
urlSearchParameters.append(key, value);
}
const dataDetailsData = await got
.get(url, {
searchParams: searchParameters,
})
.json<T>();
return dataDetailsData;
} catch (error) {
return throwErrorWithAdditionalMessage(
error,
`Fetching ${props.url} in gotGetFetcher function failed`,
);
}
};
import got, { type Options } from "got";
import { type z } from "zod";
import { throwErrorWithAdditionalMessage } from "./throwErrorWithAdditionalMessage";
import { validateZodData } from "./validateZodData";
type GotPostZodValidationType<
TInput extends z.Schema,
TOutput extends z.Schema
> = {
headers: Options["headers"];
inputData: z.infer<TInput>;
inputSchema: TInput;
inputSchemaName: string;
outputSchema: TOutput;
outputSchemaName: string;
url: string;
};
const map = new Map();
export const gotPostZodValidation = async <
TInput extends z.Schema,
TOutput extends z.Schema
>({
url,
inputData,
inputSchema,
inputSchemaName,
outputSchema,
outputSchemaName,
headers,
}: GotPostZodValidationType<TInput, TOutput>): Promise<z.infer<TOutput>> => {
try {
const parsedInputData = await validateZodData({
data: inputData,
schema: inputSchema,
schemaName: inputSchemaName,
});
const dataDetailsData = await got
.post(url, {
headers,
cache: map,
json: parsedInputData,
})
.json<z.infer<TOutput>>();
return await validateZodData({
data: dataDetailsData,
schema: outputSchema,
schemaName: outputSchemaName,
});
} catch (error) {
return throwErrorWithAdditionalMessage(
error,
`Fetching ${url} in gotPostZodValidation function failed`
);
}
};
/**
* Represents an HTTP exception.
* @class
* @augments Error
*/
export class HttpException extends Error {
/**
* The HTTP status code of the exception.
* @type {number}
*/
public status: number;
/**
* The message of the exception.
* @type {string}
*/
public message: string;
/**
* Creates a new instance of HttpException.
* @class
* @param {number} status - The HTTP status code of the exception.
* @param {string} message - The message of the exception.
*/
public constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
import { type NextFunction, type Request, type Response } from "express";
import { verify } from "jsonwebtoken";
import { API_KEY, PIN } from "../config/index.js";
import { HttpException } from "../exceptions/httpException.js";
import { type DataStoredInJwtToken } from "../services/authToken.js";
/**
* Gets the authorization token from the request headers.
* @param {Request} request - The request object.
* @returns {string | null} The authorization token, or null if not found.
*/
const getAuthorizationToken = (request: Request): string | null => {
const header = request.header("Authorization");
if (header) {
const jwt = header.split("JWT ")[1];
if (jwt) return jwt;
return null;
}
return null;
};
/**
* Express middleware that checks if a request is authenticated.
* @param {Request} request - The request object.
* @param {Response} _response - The response object.
* @param {NextFunction} next - The next function in the middleware chain.
* @throws {HttpException} If the authentication token is missing or invalid.
*/
export const jwtAuthMiddleware = (
request: Request,
_response: Response,
next: NextFunction,
) => {
try {
const token = getAuthorizationToken(request);
if (token) {
const payload = verify(token, API_KEY) as DataStoredInJwtToken;
if (payload.pin === PIN) {
next();
} else {
next(new HttpException(401, "JWT token mismatch"));
}
} else {
next(new HttpException(401, "JWT token missing"));
}
} catch {
next(new HttpException(401, "Wrong JWT token"));
}
};
import { type Request, type Response } from "express";
import { logger } from "./logger.js";
/**
* Logs the error message and sends a JSON response with the error message and status code.
* @function
* @param {unknown} error - The error to log.
* @param {Request} request - The Express request object.
* @param {Response} response - The Express response object.
* @param {number} status - The HTTP status code.
* @param {string} loggerMessage - The error message for logger.
* @param {string} errorMessage - The error message for the response.
* @returns {void}
*/
export function logErrorResponse(
error: unknown,
request: Request,
response: Response,
status: number,
loggerMessage: string,
errorMessage: string = loggerMessage,
): void {
logger.error(
`[${request.method}] ${request.path} >> StatusCode:: ${status}, Message:: ${loggerMessage} -`,
error,
);
response.status(status).json({ message: errorMessage });
}
import { consola } from "consola";
import { createLogger, format, transports } from "winston";
import winstonDaily from "winston-daily-rotate-file";
import { LOG_DIR, NODE_ENV } from "../config/index.js";
import { ensureDirectoryAccessSync } from "./ensureDirectoryAccessSync.js";
const logDirectory = LOG_DIR;
ensureDirectoryAccessSync(logDirectory);
const { combine, timestamp, printf, colorize, errors, prettyPrint } = format;
// Define log format
const logFormat = printf(
({ level, message, timestamp: _timestamp }) =>
`${_timestamp} ${level}: ${message}`,
);
const jsonLogFileFormat = combine(
// Error stack is not needed for production
// errors({ stack: true }),
timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
logFormat,
prettyPrint(),
);
const isDevelopment = NODE_ENV === "development";
/*
* Log Level
* error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
*/
const winstonLogger = createLogger({
format: jsonLogFileFormat,
transports: [
// debug log setting
new winstonDaily({
level: "debug",
datePattern: "YYYY-MM-DD",
// log file /logs/debug/*.log in save
dirname: logDirectory + "/debug",
filename: `%DATE%.log`,
// 30 Days saved
maxFiles: 30,
json: false,
zippedArchive: true,
}),
// error log setting
new winstonDaily({
level: "error",
datePattern: "YYYY-MM-DD",
// log file /logs/error/*.log in save
dirname: logDirectory + "/error",
filename: `%DATE%.log`,
// 30 Days saved
maxFiles: 30,
handleExceptions: true,
json: false,
zippedArchive: true,
}),
],
});
// Add console transport for logging to console
winstonLogger.add(
new transports.Console({
format: combine(
errors({ stack: true }),
colorize(),
printf(({ level, message, timestamp: _timestamp, stack }) => {
if (stack) {
// print log trace
console.error(stack);
}
return `${_timestamp} ${level}: ${message}`;
}),
),
}),
);
// Define a stream for morgan to use for logging HTTP requests
const stream = {
write: (message: string) => {
winstonLogger.info(
message.slice(0, Math.max(0, message.lastIndexOf("\n"))),
);
},
};
const logger = isDevelopment ? consola : winstonLogger;
export { logger, stream };
import fs from "node:fs/promises";
import path from "node:path";
import { ensureFileAccess } from "./ensureFileAccess.mjs";
import { isErrorObject } from "./getErrorMessage.mjs";
import { throwError } from "./throwError.mjs";
/**
* Reads JSON data from a file.
* @async
* @template T
* @param {string} directoryPath - The path of the directory to write to.
* @param {string} fileName - The name of the file to read from.
* @returns {Promise<T>} A promise that resolves with the parsed JSON data.
* @throws {Error} If the read operation fails or if the file does not exist.
*/
export async function readJsonFromFile<T>(
directoryPath: string,
fileName: string
): Promise<T> {
try {
await ensureDirectoryAccess(directoryPath);
const filePath = path.join(directoryPath, fileName);
await ensureFileAccess(filePath);
const fileData = await fs.readFile(filePath);
const stringData = fileData.toString("utf8");
const jsonData = JSON.parse(stringData || "{}");
return jsonData;
} catch (error) {
if (isErrorObject(error) && "code" in error && error.code === "ENOENT") {
return throwError(
error,
`File does not exist: ${directoryPath}/${fileName} in readJsonFromFile function`
);
}
return throwError(
error,
`Could not read from file: ${directoryPath}/${fileName} in readJsonFromFile function`
);
}
}
/* eslint-disable no-console */
// write a node script to separate an single audio sprite into individual audio files provided associated an json file
// load the json file
const json = require("./audioSprite.json");
const fs = require("node:fs");
const path = require("node:path");
const exec = require("node:child_process").exec;
let count = 0;
const file = path.resolve(__dirname, "audioSprite.mp3");
// fix bug that the json is not iterable
if (typeof json[Symbol.iterator] !== "function") {
json[Symbol.iterator] = function* () {
for (const key of Object.keys(this)) {
yield this[key];
}
};
}
for (const item of json) {
console.log("🚀 ~ file: sprite.cjs:23 ~ item:", item);
const start = item.start;
console.log("🚀 ~ file: sprite.cjs:25 ~ start:", start);
const end = item.end;
console.log("🚀 ~ file: sprite.cjs:27 ~ end:", end);
const name = item.name;
const cmd =
"ffmpeg -i " +
file +
" -ss " +
start +
" -to " +
end +
" -acodec copy " +
name +
".mp3";
// split the audio file give the start and end time using ffmpeg
// eslint-disable-next-line no-loop-func
exec(cmd, (error) => {
if (error) {
console.log("error with " + name + " " + error);
} else {
console.log(name + " done");
}
count++;
if (count === json.length) {
console.log("all done");
}
});
}
import { addAdditionalErrorMessage } from "./addAdditionalErrorMessage.js";
/**
* Throws an error with an additional error message.
* @param error - The original error.
* @param errorMessage - The additional error message.
* @throws {Error}
*/
export function throwError(error: unknown, errorMessage: string): never {
throw new Error(addAdditionalErrorMessage(error, errorMessage), {
cause: error,
});
}
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
import { throwErrorWithAdditionalMessage } from "./throwErrorWithAdditionalMessage.js";
type ValidateZodDataType<T extends z.Schema> = {
data: z.infer<T>;
schema: T;
schemaName: string;
};
export const validateZodData = async <T extends z.Schema>({
data,
schema,
schemaName,
}: ValidateZodDataType<T>): Promise<z.infer<T>> => {
try {
return await schema.parseAsync(data);
} catch (error) {
if (error instanceof z.ZodError) {
const validationError = fromZodError(error, { prefix: schemaName });
return throwErrorWithAdditionalMessage(
validationError,
`Validating ${schemaName} in validateZodData function failed`
);
}
return throwErrorWithAdditionalMessage(
error,
`Validating ${schemaName} in validateZodData function failed`
);
}
};
import { type NextFunction, type Request, type Response } from "express";
import { ZodError, type AnyZodObject } from "zod";
import { fromZodError } from "zod-validation-error";
import { HttpException } from "../exceptions/httpException.js";
import { addAdditionalErrorMessage } from "../utils/addAdditionalErrorMessage.js";
import { type AsyncHandler } from "../utils/asyncWrapper.js";
import { logger } from "../utils/logger.js";
/**
* Represents the validation middleware.
* @function
* @param {object} options - The options for the validation middleware.
* @param {AnyZodObject} options.schema - The Zod schema to validate the request body against.
* @param {string} options.schemaName - The name of the schema being validated.
* @returns {Function} - The validation middleware function.
*/
export function validationMiddleware({
schema,
schemaName,
}: {
schema: AnyZodObject;
schemaName: string;
}): AsyncHandler {
/**
* The middleware function that validates the request body against the Zod schema.
* @function
* @param {Request} request - The request object.
* @param {Response} _response - The response object.
* @param {NextFunction} next - The next middleware function.
* @throws {HttpException} If the parsing is invalid.
*/
return async function (
request: Request,
_response: Response,
next: NextFunction,
) {
try {
await schema.parseAsync(request.body);
next();
} catch (error) {
if (error instanceof ZodError) {
const validationError = fromZodError(error, { prefix: schemaName });
logger.error(
addAdditionalErrorMessage(
validationError,
`"ParseAsync request body schema ${schemaName} errored in the validationMiddleware function"`,
),
);
next(
new HttpException(
400,
"Validating request body errored in the zod schema",
),
);
}
next(new HttpException(400, "Error validating request body"));
}
};
}
import fs from "node:fs/promises";
import path from "node:path";
import { ensureDirectoryAccess } from "./ensureDirectoryAccess.js";
import { throwError } from "./throwError.js";
/**
* Writes JSON data to a file.
* @async
* @template T
* @param {string} directoryPath - The path of the directory to write to.
* @param {string} fileName - The name of the file to write to.
* @param {T} data - The JSON data to write.
* @returns {Promise<void>} A promise that resolves when the data has been written to the file.
* @throws {Error} If the write operation fails.
*/
export async function writeJsonToFile<T>(
directoryPath: string,
fileName: string,
data: T,
): Promise<void> {
try {
await ensureDirectoryAccess(directoryPath);
const filePath = path.join(directoryPath, fileName);
await fs.writeFile(filePath, JSON.stringify(data), "utf8");
} catch (error) {
throwError(
error,
`Could not write to file: ${directoryPath}/${fileName} in writeJsonToFile function`,
);
}
}
/**
* Asynchronously resolves Prettier options by attempting to locate and read the Prettier config file.
*
* @returns A Promise that resolves with the Prettier options object.
* @throws An error if the Prettier config file could not be located or read.
*/
async function resolvePrettierOptions(): Promise<prettier.Options> {
try {
const configFile = await prettier.resolveConfigFile();
if (!configFile) {
throw new Error("Could not resolve Prettier config file.");
}
const options = await prettier.resolveConfig(configFile);
if (!options) {
throw new Error("Could not resolve Prettier options.");
}
return options;
} catch (error: unknown) {
throw new Error("Could not resolve Prettier options.", {
cause: error,
});
}
}
const prettierOptions = await resolvePrettierOptions();
/**
* Formats the given data using Prettier with JSON parser.
*
* @param data - The data to format.
* @returns The formatted data.
*/
function formatWithPrettier(data: unknown): string {
return prettier.format(JSON.stringify(data), {
...prettierOptions,
parser: "json",
});
}
/**
* Asynchronously writes the given data to a file.
* If the file already exists, its contents are overwritten.
* If the file does not exist, it is created and the data is written to it.
*
* @param file - The file to write to.
* @param data - The data to write to the file.
* @returns A Promise that resolves when the data has been written to the file.
*/
async function writeToFile(file: string, data: unknown): Promise<void> {
try {
// Check if the file exists
await access(file);
// If it exists, rewrite the file with the new data
await writeFile(file, formatWithPrettier(data));
} catch {
// If it doesn't exist, create the file and write the data to it
await writeFile(file, formatWithPrettier(data));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment