Skip to content

Commit 47e095a

Browse files
Merge pull request #148 from Sebastian-Webster/146-add-option-type-validation
feat: add option type validation
2 parents 8f17a01 + 65e9121 commit 47e095a

File tree

8 files changed

+146
-53
lines changed

8 files changed

+146
-53
lines changed

src/constants.ts

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,104 @@
1-
const CONSTANTS = {
2-
MIN_SUPPORTED_MYSQL: '8.0.20'
3-
}
1+
import { InternalServerOptions, OptionTypeChecks } from "../types";
2+
import { randomUUID } from "crypto";
3+
import {normalize as normalizePath} from 'path'
4+
import { tmpdir } from "os";
5+
import { valid as validSemver } from "semver";
46

5-
export default CONSTANTS
7+
export const MIN_SUPPORTED_MYSQL = '8.0.20';
8+
9+
export const DEFAULT_OPTIONS_GENERATOR: () => InternalServerOptions = () => ({
10+
version: undefined,
11+
dbName: 'dbdata',
12+
logLevel: 'ERROR',
13+
portRetries: 10,
14+
downloadBinaryOnce: true,
15+
lockRetries: 1_000,
16+
lockRetryWait: 1_000,
17+
username: 'root',
18+
ignoreUnsupportedSystemVersion: false,
19+
port: 0,
20+
xPort: 0,
21+
downloadRetries: 10,
22+
initSQLString: '',
23+
_DO_NOT_USE_deleteDBAfterStopped: true,
24+
//mysqlmsn = MySQL Memory Server Node.js
25+
_DO_NOT_USE_dbPath: normalizePath(`${tmpdir()}/mysqlmsn/dbs/${randomUUID().replace(/-/g, '')}`),
26+
_DO_NOT_USE_binaryDirectoryPath: `${tmpdir()}/mysqlmsn/binaries`
27+
});
28+
29+
export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS_GENERATOR()))
30+
31+
export const LOG_LEVELS = {
32+
'LOG': 0,
33+
'WARN': 1,
34+
'ERROR': 2
35+
} as const;
36+
37+
export const INTERNAL_OPTIONS = ['_DO_NOT_USE_deleteDBAfterStopped', '_DO_NOT_USE_dbPath', '_DO_NOT_USE_binaryDirectoryPath'] as const;
38+
39+
export const OPTION_TYPE_CHECKS: OptionTypeChecks = {
40+
version: {
41+
check: (opt: any) => opt === undefined || typeof opt === 'string' && validSemver(opt) !== null,
42+
errorMessage: 'Option version must be either undefined or a valid semver string.'
43+
},
44+
dbName: {
45+
check: (opt: any) => opt === undefined || typeof opt === 'string' && opt.length <= 64,
46+
errorMessage: 'Option dbName must be either undefined or a string that is not longer than 64 characters.'
47+
},
48+
logLevel: {
49+
check: (opt: any) => opt === undefined || Object.keys(LOG_LEVELS).includes(opt),
50+
errorMessage: 'Option logLevel must be either undefined or "LOG", "WARN", or "ERROR".'
51+
},
52+
portRetries: {
53+
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
54+
errorMessage: 'Option portRetries must be either undefined, a positive number, or 0.'
55+
},
56+
downloadBinaryOnce: {
57+
check: (opt: any) => opt === undefined || typeof opt === 'boolean',
58+
errorMessage: 'Option downloadBinaryOnce must be either undefined or a boolean.'
59+
},
60+
lockRetries: {
61+
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
62+
errorMessage: 'Option lockRetries must be either undefined, a positive number, or 0.'
63+
},
64+
lockRetryWait: {
65+
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
66+
errorMessage: 'Option lockRetryWait must be either undefined, a positive number, or 0.'
67+
},
68+
username: {
69+
check: (opt: any) => opt === undefined || typeof opt === 'string' && opt.length <= 32,
70+
errorMessage: 'Option username must be either undefined or a string that is not longer than 32 characters.'
71+
},
72+
ignoreUnsupportedSystemVersion: {
73+
check: (opt: any) => opt === undefined || typeof opt === 'boolean',
74+
errorMessage: 'Option ignoreUnsupportedSystemVersion must be either undefined or a boolean.'
75+
},
76+
port: {
77+
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0 && opt <= 65535,
78+
errorMessage: 'Option port must be either undefined or a number that is between 0 and 65535 inclusive.'
79+
},
80+
xPort: {
81+
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0 && opt <= 65535,
82+
errorMessage: 'Option xPort must be either undefined or a number that is between 0 and 65535 inclusive.'
83+
},
84+
downloadRetries: {
85+
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
86+
errorMessage: 'Option downloadRetries must be either undefined, a positive number, or 0.'
87+
},
88+
initSQLString: {
89+
check: (opt: any) => opt === undefined || typeof opt === 'string',
90+
errorMessage: 'Option initSQLString must be either undefined or a string.'
91+
},
92+
_DO_NOT_USE_deleteDBAfterStopped: {
93+
check: (opt: any) => opt === undefined || typeof opt === 'boolean',
94+
errorMessage: 'Option _DO_NOT_USE_deleteDBAfterStopped must be either undefined or a boolean.'
95+
},
96+
_DO_NOT_USE_dbPath: {
97+
check: (opt: any) => opt === undefined || typeof opt === 'string',
98+
errorMessage: 'Option _DO_NOT_USE_dbPath must be either undefined or a string.'
99+
},
100+
_DO_NOT_USE_binaryDirectoryPath: {
101+
check: (opt: any) => opt === undefined || typeof opt === 'string',
102+
errorMessage: 'Option _DO_NOT_USE_binaryDirectoryPath must be either undefined or a string.'
103+
}
104+
} as const;

src/index.ts

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,54 @@ import Logger from './libraries/Logger'
22
import * as os from 'node:os'
33
import Executor from "./libraries/Executor"
44
import { satisfies, lt } from "semver"
5-
import DBDestroySignal from "./libraries/AbortSignal"
65
import { BinaryInfo, InternalServerOptions, ServerOptions } from '../types'
76
import getBinaryURL from './libraries/Version'
87
import MySQLVersions from './versions.json'
98
import { downloadBinary } from './libraries/Downloader'
10-
import { randomUUID } from "crypto";
11-
import {normalize as normalizePath} from 'path'
12-
import CONSTANTS from './constants'
9+
import { MIN_SUPPORTED_MYSQL, DEFAULT_OPTIONS_KEYS, OPTION_TYPE_CHECKS, INTERNAL_OPTIONS, DEFAULT_OPTIONS_GENERATOR } from './constants'
1310

1411
export async function createDB(opts?: ServerOptions) {
15-
const defaultOptions: InternalServerOptions = {
16-
dbName: 'dbdata',
17-
logLevel: 'ERROR',
18-
portRetries: 10,
19-
downloadBinaryOnce: true,
20-
lockRetries: 1_000,
21-
lockRetryWait: 1_000,
22-
username: 'root',
23-
deleteDBAfterStopped: true,
24-
//mysqlmsn = MySQL Memory Server Node.js
25-
dbPath: normalizePath(`${os.tmpdir()}/mysqlmsn/dbs/${randomUUID().replace(/-/g, '')}`),
26-
ignoreUnsupportedSystemVersion: false,
27-
port: 0,
28-
xPort: 0,
29-
binaryDirectoryPath: `${os.tmpdir()}/mysqlmsn/binaries`,
30-
downloadRetries: 10,
31-
initSQLString: ''
32-
}
33-
3412
const suppliedOpts = opts || {};
3513
const suppliedOptsKeys = Object.keys(suppliedOpts);
36-
const internalOpts = ['_DO_NOT_USE_deleteDBAfterStopped', '_DO_NOT_USE_dbPath', '_DO_NOT_USE_binaryDirectoryPath'];
3714

38-
for (const opt of internalOpts) {
15+
for (const opt of INTERNAL_OPTIONS) {
3916
if (suppliedOptsKeys.includes(opt)) {
4017
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.`)
4118
}
4219
}
20+
21+
const options = DEFAULT_OPTIONS_GENERATOR();
4322

44-
const options: InternalServerOptions = {...defaultOptions, ...opts}
23+
for (const opt of suppliedOptsKeys) {
24+
if (!DEFAULT_OPTIONS_KEYS.includes(opt)) {
25+
throw `Option ${opt} is not a valid option.`
26+
}
27+
28+
if (!OPTION_TYPE_CHECKS[opt].check(suppliedOpts[opt])) {
29+
//Supplied option failed the check
30+
throw OPTION_TYPE_CHECKS[opt].errorMessage
31+
}
32+
33+
if (suppliedOpts[opt] !== undefined) {
34+
options[opt] = suppliedOpts[opt]
35+
}
36+
}
4537

4638
const logger = new Logger(options.logLevel)
4739

4840
const executor = new Executor(logger)
4941

5042
const version = await executor.getMySQLVersion(options.version)
5143

52-
const unsupportedMySQLIsInstalled = version && lt(version.version, CONSTANTS.MIN_SUPPORTED_MYSQL)
44+
const unsupportedMySQLIsInstalled = version && lt(version.version, MIN_SUPPORTED_MYSQL)
5345

5446
const throwUnsupportedError = unsupportedMySQLIsInstalled && !options.ignoreUnsupportedSystemVersion && !options.version
5547

5648
if (throwUnsupportedError) {
5749
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.`
5850
}
5951

60-
if (options.version && lt(options.version, CONSTANTS.MIN_SUPPORTED_MYSQL)) {
52+
if (options.version && lt(options.version, MIN_SUPPORTED_MYSQL)) {
6153
//The difference between the throw here and the throw above is this throw is because the selected "version" is not supported.
6254
//The throw above is because the system-installed MySQL is out of date and "ignoreUnsupportedSystemVersion" is not set to true.
6355
throw `The selected version of MySQL (${options.version}) is not currently supported by this package. Please choose a different version to use.`

src/libraries/Downloader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ function extractBinary(url: string, archiveLocation: string, extractedLocation:
181181
export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOptions, logger: Logger): Promise<string> {
182182
return new Promise(async (resolve, reject) => {
183183
const {url, version} = binaryInfo;
184-
const dirpath = options.binaryDirectoryPath
184+
const dirpath = options._DO_NOT_USE_binaryDirectoryPath
185185
logger.log('Binary path:', dirpath)
186186
await fsPromises.mkdir(dirpath, {recursive: true})
187187

src/libraries/Executor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class Executor {
9898
if (portIssue || xPortIssue) {
9999
this.logger.log('Error log when exiting for port in use error:', errorLog)
100100
try {
101-
await this.#deleteDatabaseDirectory(options.dbPath)
101+
await this.#deleteDatabaseDirectory(options._DO_NOT_USE_dbPath)
102102
} catch (e) {
103103
this.logger.error(e)
104104
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 {
107107
}
108108

109109
try {
110-
if (options.deleteDBAfterStopped) {
110+
if (options._DO_NOT_USE_deleteDBAfterStopped) {
111111
await this.#deleteDatabaseDirectory(dbPath)
112112
}
113113
} catch (e) {
@@ -400,15 +400,15 @@ class Executor {
400400

401401
this.logger.log('Writing init file')
402402

403-
await fsPromises.writeFile(`${options.dbPath}/init.sql`, initText, {encoding: 'utf8'})
403+
await fsPromises.writeFile(`${options._DO_NOT_USE_dbPath}/init.sql`, initText, {encoding: 'utf8'})
404404

405405
this.logger.log('Finished writing init file')
406406
}
407407

408408
async startMySQL(options: InternalServerOptions, binaryFilepath: string): Promise<MySQLDB> {
409409
let retries = 0;
410410

411-
const datadir = normalizePath(`${options.dbPath}/data`)
411+
const datadir = normalizePath(`${options._DO_NOT_USE_dbPath}/data`)
412412

413413
do {
414414
await this.#setupDataDirectories(options, binaryFilepath, datadir, true);
@@ -420,7 +420,7 @@ class Executor {
420420

421421
try {
422422
this.logger.log('Starting MySQL process')
423-
const resolved = await this.#startMySQLProcess(options, port, mySQLXPort, datadir, options.dbPath, binaryFilepath)
423+
const resolved = await this.#startMySQLProcess(options, port, mySQLXPort, datadir, options._DO_NOT_USE_dbPath, binaryFilepath)
424424
this.logger.log('Starting process was successful')
425425
return resolved
426426
} catch (e) {

src/libraries/Logger.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { LOG_LEVEL } from "../../types";
2-
3-
const LOG_LEVELS = {
4-
'LOG': 0,
5-
'WARN': 1,
6-
'ERROR': 2
7-
}
2+
import { LOG_LEVELS } from "../constants";
83

94
class Logger {
105
LOG_LEVEL: number;

stress-tests/stress.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ for (let i = 0; i < 100; i++) {
1818
const options: ServerOptions = {
1919
username: 'dbuser',
2020
logLevel: 'LOG',
21-
deleteDBAfterStopped: !process.env.useCIDBPath,
21+
_DO_NOT_USE_deleteDBAfterStopped: !process.env.useCIDBPath,
2222
ignoreUnsupportedSystemVersion: true
2323
}
2424

2525
if (process.env.useCIDBPath) {
26-
options.dbPath = `${dbPath}/${i}`
27-
options.binaryDirectoryPath = binaryPath
26+
options._DO_NOT_USE_dbPath = `${dbPath}/${i}`
27+
options._DO_NOT_USE_binaryDirectoryPath = binaryPath
2828
}
2929

3030
const db = await createDB(options)

tests/sql.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ beforeEach(async () => {
1818
const options: ServerOptions = {
1919
username: 'root',
2020
logLevel: 'LOG',
21-
deleteDBAfterStopped: !process.env.useCIDBPath,
21+
_DO_NOT_USE_deleteDBAfterStopped: !process.env.useCIDBPath,
2222
ignoreUnsupportedSystemVersion: true
2323
}
2424

2525
if (process.env.useCIDBPath) {
26-
options.dbPath = `${dbPath}/${randomUUID()}`
27-
options.binaryDirectoryPath = binaryPath
26+
options._DO_NOT_USE_dbPath = `${dbPath}/${randomUUID()}`
27+
options._DO_NOT_USE_binaryDirectoryPath = binaryPath
2828
}
2929

3030
db = await createDB(options)

types/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ export type InternalServerOptions = {
3030
lockRetries: number,
3131
lockRetryWait: number,
3232
username: string,
33-
deleteDBAfterStopped: boolean,
34-
dbPath: string,
3533
ignoreUnsupportedSystemVersion: boolean,
3634
port: number,
3735
xPort: number,
38-
binaryDirectoryPath: string,
3936
downloadRetries: number,
4037
initSQLString: string
38+
_DO_NOT_USE_deleteDBAfterStopped: boolean,
39+
_DO_NOT_USE_dbPath: string,
40+
_DO_NOT_USE_binaryDirectoryPath: string,
4141
}
4242

4343
export type ExecutorOptions = {
@@ -76,4 +76,11 @@ export type InstalledMySQLVersion = {
7676
export type BinaryInfo = {
7777
url: string,
7878
version: string
79+
}
80+
81+
export type OptionTypeChecks = {
82+
[key in keyof Required<ServerOptions>]: {
83+
check: (opt: any) => boolean,
84+
errorMessage: string
85+
}
7986
}

0 commit comments

Comments
 (0)