Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cli creation capability #143

Merged
merged 17 commits into from
Nov 24, 2024
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
15 changes: 8 additions & 7 deletions .github/ISSUE_TEMPLATE/bug-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ assignees: ''
--
Write a clear and concise description of what the bug is here.

**To Reproduce**
--
Write the steps to reproduce the behavior here:
1. ...
2. ...
3. ...
4. ...
**Command Line Invocation**
<!-- If you are able to replicate this bug by running mysql-memory-server from the CLI, please replace "Not Applicable" with the command you used to invoke the package -->
Not Applicable

**Reproducible Code Example**
<!-- If you are able to replicate this bug with application code that uses this package, please replace "Not Applicable" with the code that reproduces the bug -->
Not Applicable

**Expected behavior**
--
Expand All @@ -42,6 +42,7 @@ None
- OS and OS version (e.g macOS 15.0.1):
- JS runtime and runtime version (E.g Node.js 22.9.0):
- mysql-memory-server version:
- In which environment does this bug occur? (Application Code, CLI, or both):

**Additional context**
--
Expand Down
40 changes: 29 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MySQL Memory Server

This package allows you to create ephemeral MySQL databases from JavaScript and/or TypeScript code, great for testing. When creating a new database, if the version selected is not installed on the system, the binary is downloaded from MySQL's CDN (cdn.mysql.com)
This package allows you to create ephemeral MySQL databases from JavaScript and/or TypeScript code and also the CLI, great for testing, CI, and learning MySQL. When creating a new database, if the version selected is not installed on the system, the binary is downloaded from MySQL's CDN (cdn.mysql.com)

You can run multiple MySQL databases with this package at the same time. Each database will use a random free port. The databases will automatically shutdown when the JS runtime process exits. A `stop()` method is also provided to stop each database instance.

Expand Down Expand Up @@ -30,7 +30,7 @@ Requirements for Linux:
- If using the system installed MySQL server: 8.0.20 and newer
- If not using the system installed MySQL server: 8.0.39, 8.0.40, 8.1.0, 8.2.0, 8.3.0, 8.4.2, 8.4.3, 9.0.1, 9.1.0

## Usage
## Example Usage - Application Code

This package supports both ESM and CJS so you can use import or require.

Expand All @@ -45,7 +45,7 @@ const db = await createDB()

//Create a new database with custom options set
const db = await createDB({
// see Options for the options you can use in this object and their default values
// see Options below for the options you can use in this object and their default values
// for example:
version: '8.4.x'
})
Expand Down Expand Up @@ -73,6 +73,14 @@ await db.stop()
MySQL database initialization can take some time. If you run into a "Timeout exceeded" error with your tests, the timeout should be extended.
If using Jest, information about how to do this can be found here: https://jestjs.io/docs/jest-object#jestsettimeouttimeout

## Example Usage - CLI

```sh
# Options are added by doing --{optionName} {optionValue}
# See Options below for the options you can use with this package
npx mysql-memory-server --version 8.4.x
```

## Documentation

##### `createDB(options: ServerOptions): Promise<MySQLDB>`
Expand All @@ -93,7 +101,7 @@ If on Windows, this is the name of the named pipe that the MySQL X Plugin is lis
- `stop: () => Promise<void>`
The method to stop the database. The returned promise resolves when the database has successfully stopped.

###### Options:
#### Options:
- `version: string`

Required: No
Expand Down Expand Up @@ -213,28 +221,38 @@ The internal queries that are ran before the queries in ```initSQLString``` are
***
### :warning: Internal Options :warning:

The following options are only meant for internal debugging use. Their behaviour may change or they may get removed between major/minor/patch versions and they are not to be considered stable. The options below will not follow Semantic Versioning so it is advised to not use them.
The following options are only meant for internal use (such as CI or the internals for running this package via the CLI). Their behaviour may change or they may get removed between major/minor/patch versions and they are not to be considered stable. The options below will not follow Semantic Versioning so it is advised to not use them.

- `_DO_NOT_USE_deleteDBAfterStopped: boolean`

Required: No

Default: true

Description: Changes whether or not the database will be deleted after it has been stopped. If set to `true`, the database WILL be deleted after it has been stopped.

- `_DO_NOT_USE_dbPath: string`

Required: No

Default: `TMPDIR/mysqlmsn/dbs/UUID` (replacing TMPDIR with the OS temp directory and UUID with a UUIDv4 without seperating dashes).

Description: The folder to store database-related data in

- `_DO_NOT_USE_binaryDirectoryPath: string`

Required: No

Default: `TMPDIR/mysqlmsn/binaries` (replacing TMPDIR with the OS temp directory)

Description: The folder to store the MySQL binaries when they are downloaded from the CDN.

- `_DO_NOT_USE_beforeSignalCleanupMessage: string`

Required: No

Default: undefined

Description: The message to get displayed in the console before the cleanup that happens when the Node.js process is stopped without the ```stop()``` method being called first.

- `_DO_NOT_USE_afterSignalCleanupMessage: string`

Required: No

Default: undefined

Description: The message to get displayed in the console after the cleanup that happens when the Node.js process is stopped without the ```stop()``` method being called first.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
"bugs": {
"url": "https://github.com/Sebastian-Webster/mysql-memory-server-nodejs/issues"
},
"homepage": "https://github.com/Sebastian-Webster/mysql-memory-server-nodejs"
"homepage": "https://github.com/Sebastian-Webster/mysql-memory-server-nodejs",
"bin": "dist/src/cli.js"
}
46 changes: 46 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env node
import { createDB } from "./index";
import { OPTION_TYPE_CHECKS } from "./constants";

async function main() {
const definedOptions = process.argv.filter((option) => option.startsWith('--'))
const options = {
_DO_NOT_USE_beforeSignalCleanupMessage: '\nShutting down the epehemeral MySQL database and cleaning all related files...',
_DO_NOT_USE_afterSignalCleanupMessage: 'Shutdown and cleanup is complete.'
}
for (const opt of definedOptions) {
const index = process.argv.indexOf(opt)
const optionValue = process.argv[index + 1]

if (optionValue === undefined) {
throw `Option ${opt} must have a value.`
}

const optionName = opt.slice(2)
const optionType = OPTION_TYPE_CHECKS[optionName].definedType;

//Try to convert the options to their correct types.
//We do not need to do any proper type validation here as the library will make sure everything is correct.
//Like for example, if a string is passed to a number option, it'll be converted to NaN here, but the library
//will throw an error for it not being an actual number.
if (optionType === 'boolean') {
if (optionValue === 'true') {
options[optionName] = true
} else if (optionValue === 'false') {
options[optionName] = false
} else {
options[optionName] = optionValue
}
} else if (optionType === 'number') {
options[optionName] = parseInt(optionValue)
} else {
options[opt.slice(2)] = optionValue
}
}
console.log('Creating ephemeral MySQL database...')
const db = await createDB(options);
console.log(`A MySQL databases has been successfully created with the following parameters:\n\nUsername: ${db.username} \nDatabase Name: ${db.dbName} \nPort: ${db.port} \nX Plugin Port: ${db.xPort} \nSocket: ${db.socket} \nX Plugin Socket: ${db.xSocket}\n`)
console.log(`If you want to use the MySQL CLI client to connect to the database, you can use either commands: \nmysql -u ${db.username} -P ${db.port} --protocol tcp \nOR\nmysql -u ${db.username} --socket ${db.socket}`)
}

main()
64 changes: 46 additions & 18 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export const DEFAULT_OPTIONS_GENERATOR: () => InternalServerOptions = () => ({
_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`
_DO_NOT_USE_binaryDirectoryPath: `${tmpdir()}/mysqlmsn/binaries`,
_DO_NOT_USE_beforeSignalCleanupMessage: '',
_DO_NOT_USE_afterSignalCleanupMessage: ''
});

export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS_GENERATOR()))
Expand All @@ -34,71 +36,97 @@ export const LOG_LEVELS = {
'ERROR': 2
} as const;

export const INTERNAL_OPTIONS = ['_DO_NOT_USE_deleteDBAfterStopped', '_DO_NOT_USE_dbPath', '_DO_NOT_USE_binaryDirectoryPath'] as const;
export const INTERNAL_OPTIONS = ['_DO_NOT_USE_deleteDBAfterStopped', '_DO_NOT_USE_dbPath', '_DO_NOT_USE_binaryDirectoryPath', '_DO_NOT_USE_beforeSignalCleanup', '_DO_NOT_USE_afterSignalCleanup'] 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.'
errorMessage: 'Option version must be either undefined or a valid semver string.',
definedType: '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.'
errorMessage: 'Option dbName must be either undefined or a string that is not longer than 64 characters.',
definedType: 'string'
},
logLevel: {
check: (opt: any) => opt === undefined || Object.keys(LOG_LEVELS).includes(opt),
errorMessage: 'Option logLevel must be either undefined or "LOG", "WARN", or "ERROR".'
errorMessage: 'Option logLevel must be either undefined or "LOG", "WARN", or "ERROR".',
definedType: 'string'
},
portRetries: {
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
errorMessage: 'Option portRetries must be either undefined, a positive number, or 0.'
errorMessage: 'Option portRetries must be either undefined, a positive number, or 0.',
definedType: 'number'
},
downloadBinaryOnce: {
check: (opt: any) => opt === undefined || typeof opt === 'boolean',
errorMessage: 'Option downloadBinaryOnce must be either undefined or a boolean.'
errorMessage: 'Option downloadBinaryOnce must be either undefined or a boolean.',
definedType: 'boolean'
},
lockRetries: {
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
errorMessage: 'Option lockRetries must be either undefined, a positive number, or 0.'
errorMessage: 'Option lockRetries must be either undefined, a positive number, or 0.',
definedType: 'number'
},
lockRetryWait: {
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
errorMessage: 'Option lockRetryWait must be either undefined, a positive number, or 0.'
errorMessage: 'Option lockRetryWait must be either undefined, a positive number, or 0.',
definedType: 'number'
},
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.'
errorMessage: 'Option username must be either undefined or a string that is not longer than 32 characters.',
definedType: 'string'
},
ignoreUnsupportedSystemVersion: {
check: (opt: any) => opt === undefined || typeof opt === 'boolean',
errorMessage: 'Option ignoreUnsupportedSystemVersion must be either undefined or a boolean.'
errorMessage: 'Option ignoreUnsupportedSystemVersion must be either undefined or a boolean.',
definedType: '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.'
errorMessage: 'Option port must be either undefined or a number that is between 0 and 65535 inclusive.',
definedType: 'number'
},
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.'
errorMessage: 'Option xPort must be either undefined or a number that is between 0 and 65535 inclusive.',
definedType: 'number'
},
downloadRetries: {
check: (opt: any) => opt === undefined || typeof opt === 'number' && opt >= 0,
errorMessage: 'Option downloadRetries must be either undefined, a positive number, or 0.'
errorMessage: 'Option downloadRetries must be either undefined, a positive number, or 0.',
definedType: 'number'
},
initSQLString: {
check: (opt: any) => opt === undefined || typeof opt === 'string',
errorMessage: 'Option initSQLString must be either undefined or a string.'
errorMessage: 'Option initSQLString must be either undefined or a string.',
definedType: '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.'
errorMessage: 'Option _DO_NOT_USE_deleteDBAfterStopped must be either undefined or a boolean.',
definedType: '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.'
errorMessage: 'Option _DO_NOT_USE_dbPath must be either undefined or a string.',
definedType: '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.'
errorMessage: 'Option _DO_NOT_USE_binaryDirectoryPath must be either undefined or a string.',
definedType: 'string'
},
_DO_NOT_USE_beforeSignalCleanupMessage: {
check: (opt: any) => opt === undefined || typeof opt === 'string',
errorMessage: 'Option _DO_NOT_USE_beforeSignalCleanup must be either undefined or a string.',
definedType: 'string'
},
_DO_NOT_USE_afterSignalCleanupMessage: {
check: (opt: any) => opt === undefined || typeof opt === 'string',
errorMessage: 'Option _DO_NOT_USE_afterSignalCleanup must be either undefined or a string.',
definedType: 'string'
}
} as const;
13 changes: 0 additions & 13 deletions src/libraries/AbortSignal.ts

This file was deleted.

Loading