Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 103 additions & 4 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -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
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;
48 changes: 20 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,54 @@ 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)

const executor = new Executor(logger)

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

if (throwUnsupportedError) {
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.`
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function extractBinary(url: string, archiveLocation: string, extractedLocation:
export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOptions, logger: Logger): Promise<string> {
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})

Expand Down
10 changes: 5 additions & 5 deletions src/libraries/Executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand All @@ -107,7 +107,7 @@ class Executor {
}

try {
if (options.deleteDBAfterStopped) {
if (options._DO_NOT_USE_deleteDBAfterStopped) {
await this.#deleteDatabaseDirectory(dbPath)
}
} catch (e) {
Expand Down Expand Up @@ -400,15 +400,15 @@ 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')
}

async startMySQL(options: InternalServerOptions, binaryFilepath: string): Promise<MySQLDB> {
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);
Expand All @@ -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) {
Expand Down
7 changes: 1 addition & 6 deletions src/libraries/Logger.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
6 changes: 3 additions & 3 deletions stress-tests/stress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions tests/sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -76,4 +76,11 @@ export type InstalledMySQLVersion = {
export type BinaryInfo = {
url: string,
version: string
}

export type OptionTypeChecks = {
[key in keyof Required<ServerOptions>]: {
check: (opt: any) => boolean,
errorMessage: string
}
}