feat(cli): create DB in start command if not exists (#285)

This commit is contained in:
Ali BARIN
2022-04-08 11:36:35 +02:00
committed by GitHub
parent 75eda7f2af
commit c227dc86bb
19 changed files with 211 additions and 126 deletions

View File

@@ -1,42 +1,3 @@
import appConfig from '../../src/config/app'; import { createDatabaseAndUser } from './utils';
import logger from '../../src/helpers/logger';
import client from './client';
const createDatabaseAndUser = async () => {
if(appConfig.appEnv !== 'development' && appConfig.appEnv !== 'test') {
const errorMessage = 'Database creation can be used only with development or test environments!'
logger.error(errorMessage)
return;
}
await client.connect();
await createDatabase();
await createDatabaseUser();
await grantPrivileges();
await client.end();
}
const createDatabase = async () => {
await client.query(`CREATE DATABASE ${appConfig.postgresDatabase}`);
logger.info(`Database: ${appConfig.postgresDatabase} created!`);
}
const createDatabaseUser = async () => {
await client.query(`CREATE USER ${appConfig.postgresUsername}`);
logger.info(`Database User: ${appConfig.postgresUsername} created!`);
}
const grantPrivileges = async () => {
await client.query(
`GRANT ALL PRIVILEGES ON DATABASE ${appConfig.postgresDatabase} TO ${appConfig.postgresUsername};`
);
logger.info(
`${appConfig.postgresUsername} has granted all privileges on ${appConfig.postgresDatabase}!`
);
}
createDatabaseAndUser(); createDatabaseAndUser();

View File

@@ -1,29 +1,3 @@
import appConfig from '../../src/config/app'; import { dropDatabase } from './utils';
import logger from '../../src/helpers/logger';
import client from './client';
const dropDatabase = async () => {
if (appConfig.appEnv != 'development' && appConfig.appEnv != 'test') {
const errorMessage = 'Drop database command can be used only with development or test environments!'
logger.error(errorMessage)
return;
}
await client.connect();
await dropDatabaseAndUser();
await client.end();
}
const dropDatabaseAndUser = async() => {
await client.query(`DROP DATABASE IF EXISTS ${appConfig.postgresDatabase}`);
logger.info(`Database: ${appConfig.postgresDatabase} removed!`);
await client.query(`DROP USER IF EXISTS ${appConfig.postgresUsername}`);
logger.info(`Database User: ${appConfig.postgresUsername} removed!`);
}
dropDatabase(); dropDatabase();

View File

@@ -1,15 +1,3 @@
import User from '../../src/models/user'; import { createUser } from './utils';
import '../../src/config/orm';
import logger from '../../src/helpers/logger';
const userParams = {
email: 'user@automatisch.io',
password: 'sample',
};
async function createUser() {
const user = await User.query().insertAndFetch(userParams);
logger.info(`User has been saved: ${user.email}`);
}
createUser(); createUser();

View File

@@ -0,0 +1,100 @@
import appConfig from '../../src/config/app';
import logger from '../../src/helpers/logger';
import client from './client';
import User from '../../src/models/user';
import '../../src/config/orm';
export async function createUser(email = 'user@automatisch.io', password = 'sample') {
const UNIQUE_VIOLATION_CODE = '23505';
const userParams = {
email,
password,
};
try {
const user = await User.query().insertAndFetch(userParams);
logger.info(`User has been saved: ${user.email}`);
} catch (err) {
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
throw err;
}
logger.info(`User already exists: ${email}`);
}
}
export const createDatabaseAndUser = async (database = appConfig.postgresDatabase, user = appConfig.postgresUsername) => {
await client.connect();
await createDatabase(database);
await createDatabaseUser(user);
await grantPrivileges(database, user);
await client.end();
}
export const createDatabase = async (database = appConfig.postgresDatabase) => {
const DUPLICATE_DB_CODE = '42P04';
try {
await client.query(`CREATE DATABASE ${database}`);
logger.info(`Database: ${database} created!`);
} catch (err) {
if ((err as any).code !== DUPLICATE_DB_CODE) {
throw err;
}
logger.info(`Database: ${database} already exists!`);
}
}
export const createDatabaseUser = async (user = appConfig.postgresUsername) => {
const DUPLICATE_OBJECT_CODE = '42710';
try {
const result = await client.query(`CREATE USER ${user}`);
logger.info(`Database User: ${user} created!`);
return result;
} catch (err) {
if ((err as any).code !== DUPLICATE_OBJECT_CODE) {
throw err;
}
logger.info(`Database User: ${user} already exists!`);
}
}
export const grantPrivileges = async (
database = appConfig.postgresDatabase, user = appConfig.postgresUsername
) => {
await client.query(
`GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};`
);
logger.info(
`${user} has granted all privileges on ${database}!`
);
}
export const dropDatabase = async () => {
if (appConfig.appEnv != 'development' && appConfig.appEnv != 'test') {
const errorMessage = 'Drop database command can be used only with development or test environments!'
logger.error(errorMessage)
return;
}
await client.connect();
await dropDatabaseAndUser();
await client.end();
}
export const dropDatabaseAndUser = async(database = appConfig.postgresDatabase, user = appConfig.postgresUsername) => {
await client.query(`DROP DATABASE IF EXISTS ${database}`);
logger.info(`Database: ${database} removed!`);
await client.query(`DROP USER IF EXISTS ${user}`);
logger.info(`Database User: ${user} removed!`);
}

2
packages/backend/database.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export * as utils from './dist/bin/database/utils';
export * as database from './dist/src/config/database';

View File

@@ -0,0 +1,3 @@
/* eslint-disable */
module.exports.utils = require('./dist/bin/database/utils');
module.exports.database = require('./dist/src/config/database');

1
packages/backend/logger.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './dist/src/helpers/logger';

View File

@@ -0,0 +1,2 @@
/* eslint-disable */
module.exports = require('./dist/src/helpers/logger');

View File

@@ -3,9 +3,10 @@
"version": "0.1.0", "version": "0.1.0",
"description": "> TODO: description", "description": "> TODO: description",
"scripts": { "scripts": {
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/server.ts", "dev": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec 'ts-node' src/server.ts",
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts", "worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
"build": "tsc && yarn copy-statics", "build": "tsc && yarn copy-statics",
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
"start": "node dist/src/server.js", "start": "node dist/src/server.js",
"test": "ava", "test": "ava",
"lint": "eslint . --ignore-path ../../.eslintignore", "lint": "eslint . --ignore-path ../../.eslintignore",
@@ -53,7 +54,7 @@
"twilio": "3.70.0", "twilio": "3.70.0",
"twitch-js": "2.0.0-beta.42", "twitch-js": "2.0.0-beta.42",
"twitter-api-v2": "1.6.0", "twitter-api-v2": "1.6.0",
"winston": "^3.3.3" "winston": "^3.7.1"
}, },
"contributors": [ "contributors": [
{ {
@@ -64,11 +65,19 @@
"homepage": "https://github.com/automatisch/automatisch#readme", "homepage": "https://github.com/automatisch/automatisch#readme",
"main": "dist/src/app", "main": "dist/src/app",
"directories": { "directories": {
"bin": "bin",
"src": "src", "src": "src",
"test": "__tests__" "test": "__tests__"
}, },
"files": [ "files": [
"src" "bin",
"src",
"server.js",
"server.d.ts",
"logger.js",
"logger.d.ts",
"database.js",
"database.d.ts"
], ],
"repository": { "repository": {
"type": "git", "type": "git",

1
packages/backend/server.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './dist/src/server';

View File

@@ -0,0 +1,2 @@
/* eslint-disable */
module.exports = require('./dist/src/server.js');

View File

@@ -4,16 +4,14 @@ import type { Knex } from 'knex';
import knexConfig from '../../knexfile'; import knexConfig from '../../knexfile';
import logger from '../helpers/logger'; import logger from '../helpers/logger';
const knexInstance: Knex = knex(knexConfig); export const client: Knex = knex(knexConfig);
const CONNECTION_REFUSED = 'ECONNREFUSED'; const CONNECTION_REFUSED = 'ECONNREFUSED';
knexInstance.raw('SELECT 1') client.raw('SELECT 1')
.catch((err) => { .catch((err) => {
if (err.code === CONNECTION_REFUSED) { if (err.code === CONNECTION_REFUSED) {
logger.error('Make sure you have installed PostgreSQL and it is running.', err); logger.error('Make sure you have installed PostgreSQL and it is running.', err);
process.exit(); process.exit();
} }
}); });
export default knexInstance;

View File

@@ -1,4 +1,4 @@
import { Model } from 'objection'; import { Model } from 'objection';
import database from './database'; import { client } from './database';
Model.knex(database) Model.knex(client);

View File

@@ -1,5 +1,5 @@
import winston from 'winston' import * as winston from 'winston';
import appConfig from '../config/app' import appConfig from '../config/app';
const levels = { const levels = {
error: 0, error: 0,
@@ -7,11 +7,11 @@ const levels = {
info: 2, info: 2,
http: 3, http: 3,
debug: 4, debug: 4,
} };
const level = () => { const level = () => {
return appConfig.appEnv === 'development' ? 'debug' : 'info' return appConfig.appEnv === 'development' ? 'debug' : 'info'
} };
const colors = { const colors = {
error: 'red', error: 'red',
@@ -19,9 +19,9 @@ const colors = {
info: 'green', info: 'green',
http: 'magenta', http: 'magenta',
debug: 'white', debug: 'white',
} };
winston.addColors(colors) winston.addColors(colors);
const format = winston.format.combine( const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
@@ -29,7 +29,7 @@ const format = winston.format.combine(
winston.format.printf( winston.format.printf(
(info) => `${info.timestamp} [${info.level}]: ${info.message}`, (info) => `${info.timestamp} [${info.level}]: ${info.message}`,
), ),
) );
const transports = [ const transports = [
new winston.transports.Console(), new winston.transports.Console(),
@@ -38,13 +38,13 @@ const transports = [
level: 'error', level: 'error',
}), }),
new winston.transports.File({ filename: 'logs/server.log' }), new winston.transports.File({ filename: 'logs/server.log' }),
] ];
const logger = winston.createLogger({ export const logger = winston.createLogger({
level: level(), level: level(),
levels, levels,
format, format,
transports, transports,
}) });
export default logger export default logger;

View File

@@ -13,7 +13,7 @@ class Connection extends Base {
formattedData?: IJSONObject; formattedData?: IJSONObject;
userId!: string; userId!: string;
verified = false; verified = false;
count: number; count?: number;
static tableName = 'connections'; static tableName = 'connections';

View File

@@ -26,10 +26,5 @@
"include": [ "include": [
"src/**/*", "src/**/*",
"bin/**/*" "bin/**/*"
], ]
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
}
} }

View File

@@ -1,3 +1,4 @@
import { readFileSync } from 'fs';
import { Command, Flags } from '@oclif/core'; import { Command, Flags } from '@oclif/core';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
@@ -16,7 +17,13 @@ export default class Start extends Command {
const { flags } = await this.parse(Start); const { flags } = await this.parse(Start);
if (flags['env-file']) { if (flags['env-file']) {
dotenv.config({ path: flags['env-file'] }); const envFile = readFileSync(flags['env-file'], 'utf8');
const envConfig = dotenv.parse(envFile);
for (const key in envConfig) {
const value = envConfig[key];
process.env[key] = value;
}
} }
if (flags.env) { if (flags.env) {
@@ -26,31 +33,57 @@ export default class Start extends Command {
} }
} }
// must serve until more customization is introduced
delete process.env.SERVE_WEB_APP_SEPARATELY; delete process.env.SERVE_WEB_APP_SEPARATELY;
} }
async createDatabaseAndUser(): Promise<void> {
const { utils } = await import('@automatisch/backend/database');
await utils.createDatabaseAndUser(
process.env.POSTGRES_DATABASE,
process.env.POSTGRES_USERNAME,
);
}
async runMigrationsIfNeeded(): Promise<void> { async runMigrationsIfNeeded(): Promise<void> {
const database = (await import('@automatisch/backend/dist/src/config/database')).default; const { logger } = await import('@automatisch/backend/logger');
const migrator = database.migrate; const { database } = await import('@automatisch/backend/database');
const migrator = database.client.migrate;
const [, pendingMigrations] = await migrator.list(); const [, pendingMigrations] = await migrator.list();
const pendingMigrationsCount = pendingMigrations.length; const pendingMigrationsCount = pendingMigrations.length;
const needsToMigrate = pendingMigrationsCount > 0; const needsToMigrate = pendingMigrationsCount > 0;
if (needsToMigrate) { if (needsToMigrate) {
logger.info(`Processing ${pendingMigrationsCount} migrations.`);
await migrator.latest(); await migrator.latest();
logger.info(`Completed ${pendingMigrationsCount} migrations.`);
} else {
logger.info('No migrations needed.');
} }
} }
async seedUser(): Promise<void> {
const { utils } = await import('@automatisch/backend/database');
await utils.createUser();
}
async runApp(): Promise<void> { async runApp(): Promise<void> {
await import('@automatisch/backend/dist/src/server'); await import('@automatisch/backend/server');
} }
async run(): Promise<void> { async run(): Promise<void> {
await this.prepareEnvVars(); await this.prepareEnvVars();
await this.createDatabaseAndUser();
await this.runMigrationsIfNeeded(); await this.runMigrationsIfNeeded();
await this.seedUser();
await this.runApp(); await this.runApp();
} }
} }

View File

@@ -11,7 +11,6 @@
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "es2021", "target": "es2021",
"traceResolution": false,
"typeRoots": ["node_modules/@types", "./src/types"] "typeRoots": ["node_modules/@types", "./src/types"]
}, },
"include": ["src/**/*"] "include": ["src/**/*"]

View File

@@ -1473,6 +1473,11 @@
dependencies: dependencies:
"@bull-board/api" "3.10.1" "@bull-board/api" "3.10.1"
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@concordance/react@^2.0.0": "@concordance/react@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@concordance/react/-/react-2.0.0.tgz#aef913f27474c53731f4fd79cc2f54897de90fde" resolved "https://registry.yarnpkg.com/@concordance/react/-/react-2.0.0.tgz#aef913f27474c53731f4fd79cc2f54897de90fde"
@@ -12399,6 +12404,17 @@ logform@^2.3.2:
safe-stable-stringify "^1.1.0" safe-stable-stringify "^1.1.0"
triple-beam "^1.3.0" triple-beam "^1.3.0"
logform@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe"
integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==
dependencies:
"@colors/colors" "1.5.0"
fecha "^4.2.0"
ms "^2.1.1"
safe-stable-stringify "^2.3.1"
triple-beam "^1.3.0"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -16411,7 +16427,7 @@ safe-stable-stringify@^1.1.0:
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a"
integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw== integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==
safe-stable-stringify@^2.1.0: safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73"
integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==
@@ -17943,7 +17959,7 @@ trim@0.0.1:
resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
triple-beam@^1.2.0, triple-beam@^1.3.0: triple-beam@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
@@ -19021,29 +19037,30 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
winston-transport@^4.4.2: winston-transport@^4.5.0:
version "4.4.2" version "4.5.0"
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.2.tgz#554efe3fce229d046df006e0e3c411d240652e51" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
integrity sha512-9jmhltAr5ygt5usgUTQbEiw/7RYXpyUbEAFRCSicIacpUzPkrnQsQZSPGEI12aLK9Jth4zNcYJx3Cvznwrl8pw== integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
dependencies: dependencies:
logform "^2.3.2" logform "^2.3.2"
readable-stream "^3.4.0" readable-stream "^3.6.0"
triple-beam "^1.2.0" triple-beam "^1.3.0"
winston@^3.3.3: winston@^3.7.1:
version "3.4.0" version "3.7.2"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.4.0.tgz#7080f24b02a0684f8a37f9d5c6afb1ac23e95b84" resolved "https://registry.yarnpkg.com/winston/-/winston-3.7.2.tgz#95b4eeddbec902b3db1424932ac634f887c400b1"
integrity sha512-FqilVj+5HKwCfIHQzMxrrd5tBIH10JTS3koFGbLVWBODjiIYq7zir08rFyBT4rrTYG/eaTqDcfSIbcjSM78YSw== integrity sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng==
dependencies: dependencies:
"@dabh/diagnostics" "^2.0.2" "@dabh/diagnostics" "^2.0.2"
async "^3.2.3" async "^3.2.3"
is-stream "^2.0.0" is-stream "^2.0.0"
logform "^2.3.2" logform "^2.4.0"
one-time "^1.0.0" one-time "^1.0.0"
readable-stream "^3.4.0" readable-stream "^3.4.0"
safe-stable-stringify "^2.3.1"
stack-trace "0.0.x" stack-trace "0.0.x"
triple-beam "^1.3.0" triple-beam "^1.3.0"
winston-transport "^4.4.2" winston-transport "^4.5.0"
word-wrap@^1.2.3, word-wrap@~1.2.3: word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3" version "1.2.3"