diff --git a/src/constants.ts b/src/constants.ts index 151044ae..64056abf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,104 @@ -const CONSTANTS = { - MIN_SUPPORTED_MYSQL: '8.0.20' -} +import { InternalServerOptions, OptionTypeChecks } from "../types"; +import { randomUUID } from "crypto"; +import {normalize as normalizePath} from 'path' +import { tmpdir } from "os"; +import { valid as validSemver } from "semver"; -export default CONSTANTS \ No newline at end of file +export const MIN_SUPPORTED_MYSQL = '8.0.20'; + +export const DEFAULT_OPTIONS_GENERATOR: () => InternalServerOptions = () => ({ + version: undefined, + dbName: 'dbdata', + logLevel: 'ERROR', + portRetries: 10, + downloadBinaryOnce: true, + lockRetries: 1_000, + lockRetryWait: 1_000, + username: 'root', + ignoreUnsupportedSystemVersion: false, + port: 0, + xPort: 0, + downloadRetries: 10, + initSQLString: '', + _DO_NOT_USE_deleteDBAfterStopped: true, + //mysqlmsn = MySQL Memory Server Node.js + _DO_NOT_USE_dbPath: normalizePath(`${tmpdir()}/mysqlmsn/dbs/${randomUUID().replace(/-/g, '')}`), + _DO_NOT_USE_binaryDirectoryPath: `${tmpdir()}/mysqlmsn/binaries` +}); + +export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS_GENERATOR())) + +export const LOG_LEVELS = { + 'LOG': 0, + 'WARN': 1, + 'ERROR': 2 +} as const; + +export const INTERNAL_OPTIONS = ['_DO_NOT_USE_deleteDBAfterStopped', '_DO_NOT_USE_dbPath', '_DO_NOT_USE_binaryDirectoryPath'] as const; + +export const OPTION_TYPE_CHECKS: OptionTypeChecks = { + version: { + check: (opt: any) => opt === undefined || typeof opt === 'string' && validSemver(opt) !== null, + errorMessage: 'Option version must be either undefined or a valid semver string.' + }, + dbName: { + check: (opt: any) => opt === undefined || typeof opt === 'string' && opt.length <= 64, + errorMessage: 'Option dbName must be either undefined or a string that is not longer than 64 characters.' + }, + logLevel: { + check: (opt: any) => opt === undefined || Object.keys(LOG_LEVELS).includes(opt), + errorMessage: 'Option logLevel must be either undefined or "LOG", "WARN", or "ERROR".' + }, + portRetries: { + check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0, + errorMessage: 'Option portRetries must be either undefined, a positive number, or 0.' + }, + downloadBinaryOnce: { + check: (opt: any) => opt === undefined || typeof opt === 'boolean', + errorMessage: 'Option downloadBinaryOnce must be either undefined or a boolean.' + }, + lockRetries: { + check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0, + errorMessage: 'Option lockRetries must be either undefined, a positive number, or 0.' + }, + lockRetryWait: { + check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0, + errorMessage: 'Option lockRetryWait must be either undefined, a positive number, or 0.' + }, + username: { + check: (opt: any) => opt === undefined || typeof opt === 'string' && opt.length <= 32, + errorMessage: 'Option username must be either undefined or a string that is not longer than 32 characters.' + }, + ignoreUnsupportedSystemVersion: { + check: (opt: any) => opt === undefined || typeof opt === 'boolean', + errorMessage: 'Option ignoreUnsupportedSystemVersion must be either undefined or a boolean.' + }, + port: { + check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0 && opt <= 65535, + errorMessage: 'Option port must be either undefined or a number that is between 0 and 65535 inclusive.' + }, + xPort: { + check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0 && opt <= 65535, + errorMessage: 'Option xPort must be either undefined or a number that is between 0 and 65535 inclusive.' + }, + downloadRetries: { + check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0, + errorMessage: 'Option downloadRetries must be either undefined, a positive number, or 0.' + }, + initSQLString: { + check: (opt: any) => opt === undefined || typeof opt === 'string', + errorMessage: 'Option initSQLString must be either undefined or a string.' + }, + _DO_NOT_USE_deleteDBAfterStopped: { + check: (opt: any) => opt === undefined || typeof opt === 'boolean', + errorMessage: 'Option _DO_NOT_USE_deleteDBAfterStopped must be either undefined or a boolean.' + }, + _DO_NOT_USE_dbPath: { + check: (opt: any) => opt === undefined || typeof opt === 'string', + errorMessage: 'Option _DO_NOT_USE_dbPath must be either undefined or a string.' + }, + _DO_NOT_USE_binaryDirectoryPath: { + check: (opt: any) => opt === undefined || typeof opt === 'string', + errorMessage: 'Option _DO_NOT_USE_binaryDirectoryPath must be either undefined or a string.' + } +} as const; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b1396f56..f8a33c7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,46 +2,38 @@ import Logger from './libraries/Logger' import * as os from 'node:os' import Executor from "./libraries/Executor" import { satisfies, lt } from "semver" -import DBDestroySignal from "./libraries/AbortSignal" import { BinaryInfo, InternalServerOptions, ServerOptions } from '../types' import getBinaryURL from './libraries/Version' import MySQLVersions from './versions.json' import { downloadBinary } from './libraries/Downloader' -import { randomUUID } from "crypto"; -import {normalize as normalizePath} from 'path' -import CONSTANTS from './constants' +import { MIN_SUPPORTED_MYSQL, DEFAULT_OPTIONS_KEYS, OPTION_TYPE_CHECKS, INTERNAL_OPTIONS, DEFAULT_OPTIONS_GENERATOR } from './constants' export async function createDB(opts?: ServerOptions) { - const defaultOptions: InternalServerOptions = { - dbName: 'dbdata', - logLevel: 'ERROR', - portRetries: 10, - downloadBinaryOnce: true, - lockRetries: 1_000, - lockRetryWait: 1_000, - username: 'root', - deleteDBAfterStopped: true, - //mysqlmsn = MySQL Memory Server Node.js - dbPath: normalizePath(`${os.tmpdir()}/mysqlmsn/dbs/${randomUUID().replace(/-/g, '')}`), - ignoreUnsupportedSystemVersion: false, - port: 0, - xPort: 0, - binaryDirectoryPath: `${os.tmpdir()}/mysqlmsn/binaries`, - downloadRetries: 10, - initSQLString: '' - } - const suppliedOpts = opts || {}; const suppliedOptsKeys = Object.keys(suppliedOpts); - const internalOpts = ['_DO_NOT_USE_deleteDBAfterStopped', '_DO_NOT_USE_dbPath', '_DO_NOT_USE_binaryDirectoryPath']; - for (const opt of internalOpts) { + for (const opt of INTERNAL_OPTIONS) { if (suppliedOptsKeys.includes(opt)) { console.warn(`[ mysql-memory-server - Options WARN ]: Creating MySQL database with option ${opt}. This is considered unstable and should not be used externally. Please consider removing this option.`) } } + + const options = DEFAULT_OPTIONS_GENERATOR(); - const options: InternalServerOptions = {...defaultOptions, ...opts} + for (const opt of suppliedOptsKeys) { + if (!DEFAULT_OPTIONS_KEYS.includes(opt)) { + throw `Option ${opt} is not a valid option.` + } + + if (!OPTION_TYPE_CHECKS[opt].check(suppliedOpts[opt])) { + //Supplied option failed the check + throw OPTION_TYPE_CHECKS[opt].errorMessage + } + + if (suppliedOpts[opt] !== undefined) { + options[opt] = suppliedOpts[opt] + } + } const logger = new Logger(options.logLevel) @@ -49,7 +41,7 @@ export async function createDB(opts?: ServerOptions) { const version = await executor.getMySQLVersion(options.version) - const unsupportedMySQLIsInstalled = version && lt(version.version, CONSTANTS.MIN_SUPPORTED_MYSQL) + const unsupportedMySQLIsInstalled = version && lt(version.version, MIN_SUPPORTED_MYSQL) const throwUnsupportedError = unsupportedMySQLIsInstalled && !options.ignoreUnsupportedSystemVersion && !options.version @@ -57,7 +49,7 @@ export async function createDB(opts?: ServerOptions) { throw `A version of MySQL is installed on your system that is not supported by this package. If you want to download a MySQL binary instead of getting this error, please set the option "ignoreUnsupportedSystemVersion" to true.` } - if (options.version && lt(options.version, CONSTANTS.MIN_SUPPORTED_MYSQL)) { + if (options.version && lt(options.version, MIN_SUPPORTED_MYSQL)) { //The difference between the throw here and the throw above is this throw is because the selected "version" is not supported. //The throw above is because the system-installed MySQL is out of date and "ignoreUnsupportedSystemVersion" is not set to true. throw `The selected version of MySQL (${options.version}) is not currently supported by this package. Please choose a different version to use.` diff --git a/src/libraries/Downloader.ts b/src/libraries/Downloader.ts index 3af32998..b5ce8baa 100644 --- a/src/libraries/Downloader.ts +++ b/src/libraries/Downloader.ts @@ -181,7 +181,7 @@ function extractBinary(url: string, archiveLocation: string, extractedLocation: export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOptions, logger: Logger): Promise { return new Promise(async (resolve, reject) => { const {url, version} = binaryInfo; - const dirpath = options.binaryDirectoryPath + const dirpath = options._DO_NOT_USE_binaryDirectoryPath logger.log('Binary path:', dirpath) await fsPromises.mkdir(dirpath, {recursive: true}) diff --git a/src/libraries/Executor.ts b/src/libraries/Executor.ts index 5d1a83ea..5d7839ec 100644 --- a/src/libraries/Executor.ts +++ b/src/libraries/Executor.ts @@ -98,7 +98,7 @@ class Executor { if (portIssue || xPortIssue) { this.logger.log('Error log when exiting for port in use error:', errorLog) try { - await this.#deleteDatabaseDirectory(options.dbPath) + await this.#deleteDatabaseDirectory(options._DO_NOT_USE_dbPath) } catch (e) { this.logger.error(e) return reject(`MySQL failed to listen on a certain port. To restart MySQL with a different port, the database directory needed to be deleted. An error occurred while deleting the database directory. Aborting. The error was: ${e}`) @@ -107,7 +107,7 @@ class Executor { } try { - if (options.deleteDBAfterStopped) { + if (options._DO_NOT_USE_deleteDBAfterStopped) { await this.#deleteDatabaseDirectory(dbPath) } } catch (e) { @@ -400,7 +400,7 @@ class Executor { this.logger.log('Writing init file') - await fsPromises.writeFile(`${options.dbPath}/init.sql`, initText, {encoding: 'utf8'}) + await fsPromises.writeFile(`${options._DO_NOT_USE_dbPath}/init.sql`, initText, {encoding: 'utf8'}) this.logger.log('Finished writing init file') } @@ -408,7 +408,7 @@ class Executor { async startMySQL(options: InternalServerOptions, binaryFilepath: string): Promise { let retries = 0; - const datadir = normalizePath(`${options.dbPath}/data`) + const datadir = normalizePath(`${options._DO_NOT_USE_dbPath}/data`) do { await this.#setupDataDirectories(options, binaryFilepath, datadir, true); @@ -420,7 +420,7 @@ class Executor { try { this.logger.log('Starting MySQL process') - const resolved = await this.#startMySQLProcess(options, port, mySQLXPort, datadir, options.dbPath, binaryFilepath) + const resolved = await this.#startMySQLProcess(options, port, mySQLXPort, datadir, options._DO_NOT_USE_dbPath, binaryFilepath) this.logger.log('Starting process was successful') return resolved } catch (e) { diff --git a/src/libraries/Logger.ts b/src/libraries/Logger.ts index 2228f37c..5d0fdfe3 100644 --- a/src/libraries/Logger.ts +++ b/src/libraries/Logger.ts @@ -1,10 +1,5 @@ import { LOG_LEVEL } from "../../types"; - -const LOG_LEVELS = { - 'LOG': 0, - 'WARN': 1, - 'ERROR': 2 -} +import { LOG_LEVELS } from "../constants"; class Logger { LOG_LEVEL: number; diff --git a/stress-tests/stress.test.ts b/stress-tests/stress.test.ts index 514ff11e..7fab4316 100644 --- a/stress-tests/stress.test.ts +++ b/stress-tests/stress.test.ts @@ -18,13 +18,13 @@ for (let i = 0; i < 100; i++) { const options: ServerOptions = { username: 'dbuser', logLevel: 'LOG', - deleteDBAfterStopped: !process.env.useCIDBPath, + _DO_NOT_USE_deleteDBAfterStopped: !process.env.useCIDBPath, ignoreUnsupportedSystemVersion: true } if (process.env.useCIDBPath) { - options.dbPath = `${dbPath}/${i}` - options.binaryDirectoryPath = binaryPath + options._DO_NOT_USE_dbPath = `${dbPath}/${i}` + options._DO_NOT_USE_binaryDirectoryPath = binaryPath } const db = await createDB(options) diff --git a/tests/sql.test.ts b/tests/sql.test.ts index fdf44062..29b6f550 100644 --- a/tests/sql.test.ts +++ b/tests/sql.test.ts @@ -18,13 +18,13 @@ beforeEach(async () => { const options: ServerOptions = { username: 'root', logLevel: 'LOG', - deleteDBAfterStopped: !process.env.useCIDBPath, + _DO_NOT_USE_deleteDBAfterStopped: !process.env.useCIDBPath, ignoreUnsupportedSystemVersion: true } if (process.env.useCIDBPath) { - options.dbPath = `${dbPath}/${randomUUID()}` - options.binaryDirectoryPath = binaryPath + options._DO_NOT_USE_dbPath = `${dbPath}/${randomUUID()}` + options._DO_NOT_USE_binaryDirectoryPath = binaryPath } db = await createDB(options) diff --git a/types/index.ts b/types/index.ts index 08550024..8d2f9d92 100644 --- a/types/index.ts +++ b/types/index.ts @@ -30,14 +30,14 @@ export type InternalServerOptions = { lockRetries: number, lockRetryWait: number, username: string, - deleteDBAfterStopped: boolean, - dbPath: string, ignoreUnsupportedSystemVersion: boolean, port: number, xPort: number, - binaryDirectoryPath: string, downloadRetries: number, initSQLString: string + _DO_NOT_USE_deleteDBAfterStopped: boolean, + _DO_NOT_USE_dbPath: string, + _DO_NOT_USE_binaryDirectoryPath: string, } export type ExecutorOptions = { @@ -76,4 +76,11 @@ export type InstalledMySQLVersion = { export type BinaryInfo = { url: string, version: string +} + +export type OptionTypeChecks = { + [key in keyof Required]: { + check: (opt: any) => boolean, + errorMessage: string + } } \ No newline at end of file