Initial commit

This commit is contained in:
Marco Lipparini
2023-07-15 20:03:01 +02:00
commit aa8e75520b
13 changed files with 1833 additions and 0 deletions

15
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.x'
- run: yarn
- run: yarn lint

22
.github/workflows/publish-releases.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Publish Package to npmjs.org
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'
scope: '@mep-agency'
- run: yarn
- run: yarn lint && yarn build
- run: yarn publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
bin
build
ldd
# misc
*.log
.DS_Store
node_modules
.cache
dist
# dependencies
/node_modules
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

9
.npmignore Normal file
View File

@@ -0,0 +1,9 @@
.github
build
src
*.log
.DS_Store
node_modules
.cache
*.lock

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Marco Lipparini
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

116
README.md Normal file
View File

@@ -0,0 +1,116 @@
# Local Dev DB (ldd)
<span class="badge-lifecycle"><a href="https://github.com/mep-agency#lifecycle-policy" title="Check out our lifecycle stages"><img src="https://img.shields.io/badge/lifecycle-experimental-orange" alt="Project lifecyfle stage" /></a></span>
<span class="badge-license"><a href="https://github.com/mep-agency/local-dev-db" title="View this project on GitHub"><img src="https://img.shields.io/github/license/mep-agency/local-dev-db" alt="Project license" /></a></span>
<span class="badge-npmversion"><a href="https://www.npmjs.com/package/@mep-agency/local-dev-db" title="View this project on NPM"><img src="https://img.shields.io/npm/v/%40mep-agency/local-dev-db" alt="NPM version" /></a></span>
<span class="badge-npmdownloads"><a href="https://www.npmjs.com/package/@mep-agency/local-dev-db" title="View this project on NPM"><img src="https://img.shields.io/npm/dt/%40mep-agency/local-dev-db" alt="NPM downloads" /></a></span>
A zero-config local MariaDB instance for local development (using Docker) so you can finally stop doing things like:
- Using SQLite for dev and MariaDB/MySQL for production
- Installing a local database server directly
- Spending a lot of time to get up and running on a new dev environment
## How does it fit your workflow?
While this tool is meant to be installed as a dependency to your projects, it actually runs as a single database server.
This makes it possible to optimize the resources when working on multiple projects at the same time.
Feel free to install this tool as a dependency to all of your projects, CLI commands will act on the same instance and all of your databases will share the same storage volume.
## Features
- Runs a fully-featured MariaDB server without touching your local system
- Runs a PhpMyAdmin instance attached to the DB server so you can manage your databases with no additional software
- Provides you with a simple set of CLI commands do run common tasks:
- Create/drop databases and dedicated users
- Export/import SQL files (single DB or full server)
## Requirements
- **Docker:** this tool uses docker (compose) to spwan some containers for you. A basic default installation is usually more than enough (e.g. `brew install docker` or similar).
## Original author
- Marco Lipparini ([liarco](https://github.com/liarco))
## Getting started
Make sure Docker is installed and configured properly, the `docker` CLI must be available for this tool to work properly.
Simply install the package using any package manager:
```bash
# With Yarn
$ yarn add --dev @mep-agency/local-dev-db
# With NPM
$ npm install --save-dev @mep-agency/local-dev-db
```
Run the `ldd` binary to see the available commands:
```bash
# With Yarn
$ yarn ldd
Usage: ldd [options] [command]
A zero-config local MariaDB instance for local development (using Docker)
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
# ...
# With NPM
$ npx ldd
Usage: ldd [options] [command]
# ...
```
## Starting a new project
Creating a brand new database for your project is pretty easy:
```bash
$ yarn add --dev @mep-agency/local-dev-db
# ...
$ yarn ldd start
Starting local database containers...
A PhpMyAdmin instance is running on: http://127.0.0.1:8010
$ yarn ldd create my-awesome-app
Creating a new DB named "my-awesome-app"...
A new user has been created with full permissions on "my-awesome-app".
Username: my-awesome-app
Password: my-awesome-app-pwd
```
Our main focus is DX and speed, so don't expect any fancy configuration options or proper security. You can connect to the new database with simple default auth: `mysql://my-awesome-app:my-awesome-app-pwd@127.0.0.1:3306/my-awesome-app`.
You can also connect to http://127.0.0.1:8010 to access a PhpMyAdmin instance attached to your server.
Once done, you can stop your containers from any of your projects:
```bash
# This will stop all containers at once!
$ yarn ldd stop
Stopping local database containers...
```
## Advanced configuration
We hope you never have to use it, but just in case, here are some ENV vars you can set on your machine to customize the behavior of the application:
- `LDD_DB_IMAGE_TAG` (default: `latest`): we use the official [MariaDB](https://hub.docker.com/_/mariadb) Docker image. You can pick a different tag if you wish.
- `LDD_DB_PORT` (default: `3306`): The database server will be attached to this port on your local machine. You can customize this to avoid any conflicts with other services.
- `LDD_DB_ROOT_PASSWORD` (default: `not-secure-pwd`): This tool is not secure by design, so you should probably leave this untouched to avoid issues.
- `LDD_PMA_IMAGE_TAG` (default: `latest`): we use the official [PhpMyAdmin](https://hub.docker.com/_/phpmyadmin) Docker image. You can pick a different tag if you wish.
- `LDD_PMA_PORT` (default: `8010`): The PhpMyAdmin instance will be attached to this port on your local machine. You can customize this to avoid any conflicts with other services.
Changing some of these variables after the initial server creation might break it due to the way storage is persisted in volumes. For instance, if you update the `LDD_DB_ROOT_PASSWORD` then your PhpMyAdmin instance won't be able to connect to your server anymore since the root password is set at creation and won't be updated unless you destroy the server and start from scratch (`yarn ldd destroy && yarn ldd start`).

35
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,35 @@
version: '3'
services:
db:
image: "mariadb:${LDD_DB_IMAGE_TAG:-latest}"
ports:
- "${LDD_DB_PORT:-3306}:3306"
networks:
- private
volumes:
- db_data:/var/lib/mysql
environment:
MARIADB_ROOT_PASSWORD: "${LDD_DB_ROOT_PASSWORD:-not-secure-pwd}"
MARIADB_DATABASE: defaultdb
phpmyadmin:
image: "phpmyadmin:${LDD_PMA_IMAGE_TAG:-latest}"
restart: always
ports:
- "${LDD_PMA_PORT:-8010}:80"
networks:
- private
depends_on:
- db
environment:
PMA_HOST: db
PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: "${LDD_DB_ROOT_PASSWORD:-not-secure-pwd}"
MYSQL_ROOT_PASSWORD: "${LDD_DB_ROOT_PASSWORD:-not-secure-pwd}"
networks:
private:
volumes:
db_data:

58
package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "@mep-agency/local-dev-db",
"version": "1.0.0-alpha1",
"private": false,
"description": "A zero-config local MariaDB instance for local development (using Docker)",
"author": "Marco Lipparini <developer@liarco.net>",
"license": "MIT",
"keywords": [
"development",
"local",
"database",
"db",
"mysql",
"mariadb"
],
"repository": {
"type": "git",
"url": "git+https://github.com/mep-agency/local-dev-db.git"
},
"bugs": {
"url": "https://github.com/mep-agency/local-dev-db/issues"
},
"scripts": {
"build": "rm -rf ./bin && rm -rf ./build && tsc && pkg -c package.json ./build/src/index.js && yarn postinstall",
"watch": "tsc --watch",
"format": "prettier --write \"**/*.{ts,md,scss,css,js}\"",
"lint": "prettier --check \"**/*.{ts,md,scss,css,js}\"",
"postinstall": "node postinstall.js install"
},
"bin": {
"ldd": "./ldd"
},
"files": [
"bin",
"docker",
"LICENCE",
"README.md"
],
"pkg": {
"targets": [
"linux-x64",
"macos-x64",
"win-x64"
],
"outputPath": "bin"
},
"devDependencies": {
"@inquirer/prompts": "^3.0.0",
"@types/mysql": "^2.15.21",
"commander": "^11.0.0",
"docker-cli-js": "^2.10.0",
"mysql": "^2.18.1",
"pkg": "^5.8.1",
"prettier": "^2.8.8",
"typescript": "^5.1.3"
},
"dependencies": {}
}

67
postinstall.js Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
// Inspired by: https://blog.xendit.engineer/how-we-repurposed-npm-to-publish-and-distribute-our-go-binaries-for-internal-cli-23981b80911b
const fs = require('fs');
const BIN_BASE_NAME = './bin/@mep-agency/local-dev-db';
if (!fs.existsSync(BIN_BASE_NAME) || !fs.statSync(BIN_BASE_NAME).isDirectory()) {
console.info('Binaries are not available, this probably means we are in a development environment... skipping!');
process.exit(0);
}
const ARCH_MAPPING = {
x64: '',
};
const PLATFORM_MAPPING = {
darwin: 'macos',
linux: 'linux',
win32: 'win.exe',
};
async function install(callback) {
if (PLATFORM_MAPPING[process.platform] === undefined) {
callback(`Unsupported platform: "${process.platform}"`);
}
if (ARCH_MAPPING[process.arch] === undefined) {
callback(`Unsupported architecture: "${process.arch}"`);
}
const binaryNameTokens = [BIN_BASE_NAME, PLATFORM_MAPPING[process.platform], ARCH_MAPPING[process.arch]].filter(
(token) => token.length > 0,
);
console.info(`Copying the relevant binary for your platform ${process.platform} (${process.arch})`);
fs.copyFileSync(binaryNameTokens.join('-'), './ldd');
callback(null);
}
// Parse command line arguments and call the right method
var actions = {
install: install,
};
const argv = process.argv;
if (argv && argv.length > 2) {
var cmd = process.argv[2];
if (!actions[cmd]) {
console.log('Invalid command.');
process.exit(1);
}
actions[cmd](function (err) {
if (err) {
console.error(err);
process.exit(1);
} else {
process.exit(0);
}
});
}

5
prettier.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
printWidth: 125,
trailingComma: 'all',
singleQuote: true,
};

230
src/index.ts Normal file
View File

@@ -0,0 +1,230 @@
import path from 'path';
import fs from 'fs';
import { program } from 'commander';
import { confirm } from '@inquirer/prompts';
import { dockerCommand } from 'docker-cli-js';
import mysql from 'mysql';
import packageInfo from '../package.json';
let PACKAGE_INSTALLATION_PATH = path.normalize(
__dirname.startsWith('/snapshot') ? path.dirname(process.execPath) : `${__dirname}/../..`,
);
const dockerCompose: typeof dockerCommand = async (command, options) => {
try {
return await dockerCommand(
`compose --file "${PACKAGE_INSTALLATION_PATH}/docker/docker-compose.yml" --project-name "ldd" ${command}`,
{ echo: false, ...(options ?? {}) },
);
} catch (e: any) {
if (e.stderr === undefined) {
throw e;
}
console.error(`ERROR: ${e.stderr}`);
process.exit(1);
}
};
const execQuery = (query: string, database: string = 'defaultdb') => {
return new Promise((resolve) => {
const connection = mysql.createConnection({
host: '127.0.0.1',
port: Number(process.env.LDD_DB_PORT ?? '3306'),
user: 'root',
password: process.env.LDD_DB_ROOT_PASSWORD ?? 'not-secure-pwd',
database,
multipleStatements: true,
});
connection.connect((error) => {
if (error) {
if (error.code === 'ECONNREFUSED') {
console.error(`ERROR: Could't connect to the DB server. Did you forget to start it?`);
process.exit(1);
}
throw error;
}
});
connection.query(query, (error, results) => {
if (error) {
console.error(`ERROR: ${error.sqlMessage}`);
process.exit(1);
}
resolve(results);
});
connection.end();
});
};
program.name('ldd').description(packageInfo.description).version(packageInfo.version);
program
.command('start')
.description('Starts your local DB server')
.action(async (str, options) => {
console.info('Starting local database containers...');
await dockerCompose('up -d');
console.info('');
console.info(`A PhpMyAdmin instance is running on: http://127.0.0.1:${process.env.LDD_PMA_PORT ?? 8010}`);
});
program
.command('stop')
.description('Stops your local DB server')
.action(async () => {
console.info('Stopping local database containers...');
await dockerCompose('down');
});
program
.command('destroy')
.description('Stops all containers (if running) and deletes any related volumes')
.action(async () => {
const confirmation = await confirm({
message: 'This action will delete all of your data and cannot be reverted. Are you sure?',
default: false,
});
if (confirmation !== true) {
console.info('Aborting...');
process.exit(0);
}
console.info('Destroying local database containers...');
await dockerCompose('down -v');
});
program
.command('create')
.description('Creates a new database')
.argument('<db_name>', 'The database name')
.action(async (databaseName) => {
const username = databaseName;
const userPwd = `${databaseName}-pwd`;
console.info(`Creating a new DB named "${databaseName}"...`);
await execQuery(
`START TRANSACTION; CREATE DATABASE \`${databaseName}\`; CREATE USER '${username}'@'%' IDENTIFIED BY '${userPwd}'; GRANT ALL ON \`${databaseName}\`.* TO '${username}'@'%'; FLUSH PRIVILEGES; COMMIT;`,
);
console.info(`A new user has been created with full permissions on "${databaseName}".`);
console.info('');
console.info(`Username: ${username}`);
console.info(`Password: ${userPwd}`);
});
program
.command('drop')
.description('Drops the given database and its default user (if they exist)')
.argument('<db_name>', 'The database name')
.action(async (databaseName) => {
const username = databaseName;
const userPwd = `${databaseName}-pwd`;
const confirmation = await confirm({
message: `This action will delete your database "${databaseName}" and cannot be reverted. Are you sure?`,
default: false,
});
if (confirmation !== true) {
console.info('Aborting...');
process.exit(0);
}
console.info(`Dropping DB "${databaseName}" and its default user...`);
await execQuery(`DROP DATABASE IF EXISTS \`${databaseName}\`; DROP USER IF EXISTS \`${databaseName}\`;`);
});
program
.command('dump-all')
.description('Creates a SQL dump file of all databases')
.action(async () => {
const now = new Date();
const month = now.getMonth().toString().padStart(2, '0');
const date = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
const dumpFileName = `db-full-dump_${now.getFullYear()}-${month}-${date}_${hours}-${minutes}-${seconds}.sql`;
console.info(`Exporting all databases to "${dumpFileName}"...`);
fs.writeFileSync(
`./${dumpFileName}`,
(
await dockerCompose(
'exec db sh -c \'exec mariadb-dump --all-databases --lock-tables -uroot -p"$MARIADB_ROOT_PASSWORD"\'',
)
).raw,
);
});
program
.command('dump')
.description('Creates a SQL dump file of the given database')
.argument('<db_name>', 'The database name')
.action(async (databaseName) => {
const now = new Date();
const month = now.getMonth().toString().padStart(2, '0');
const date = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
const dumpFileName = `db-${databaseName}-dump_${now.getFullYear()}-${month}-${date}_${hours}-${minutes}-${seconds}.sql`;
console.info(`Exporting database to "${dumpFileName}"...`);
fs.writeFileSync(
`./${dumpFileName}`,
(
await dockerCompose(
`exec db sh -c \'exec mariadb-dump --databases "${databaseName}" --lock-tables -uroot -p"$MARIADB_ROOT_PASSWORD"\'`,
)
).raw,
);
});
program
.command('import')
.description('Runs all queries from the given SQL file')
.argument('<sql_file_path>', 'The SQL file to import')
.action(async (sqlFilePath) => {
const confirmation = await confirm({
message: 'This action will execute any SQL statement found in the given file and cannot be reverted. Are you sure?',
default: false,
});
if (confirmation !== true) {
console.info('Aborting...');
process.exit(0);
}
console.info(`Importing data from "${sqlFilePath}"...`);
if (!sqlFilePath.endsWith('.sql') || !fs.existsSync(sqlFilePath) || !fs.statSync(sqlFilePath).isFile()) {
console.error(`ERROR: Invalid SQL file`);
process.exit(1);
}
execQuery(fs.readFileSync(sqlFilePath).toString());
console.info('Done. Remember you might have to create dedicated users in order to access new databases.');
});
program.parse();

16
tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"outDir": "./build",
"target": "ESNext",
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
},
"include": ["src"],
"exclude": ["node_modules"],
}

1221
yarn.lock Normal file

File diff suppressed because it is too large Load Diff