feat(cli): run migrations and app in start command (#284)

* feat: perform pending migrations in start command

* feat: log error when DB connection is refused

* feat: log error when Redis connection is refused

* refactor: fix type errors

* fix: correct server and copy graphql schema

* fix: differentiate migrations by env

* chore: remove dev executable

* chore: fix typo in default postgresUsername

* fix: copy json files into dist folder

* chore(cli): add dev script

* chore: pull non-dev logs to info level

* feat(cli): run app in start command

* fix(backend): remove default count in Connection

* fix(cli): remove .eslintrc usage in lint script

* refactor: remove disableMigrationsListValidation

* refactor: make Step optional in ExecutionStep

* refactor: make Flow optional in Step
This commit is contained in:
Ali BARIN
2022-04-08 11:27:46 +02:00
committed by GitHub
parent 45f810b5b8
commit 75eda7f2af
34 changed files with 320 additions and 143 deletions

View File

@@ -4,12 +4,11 @@ import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import corsOptions from './config/cors-options';
import graphQLInstance from './helpers/graphql-instance';
import logger from './helpers/logger';
import morgan from './helpers/morgan';
import appAssetsHandler from './helpers/app-assets-handler';
import webUIHandler from './helpers/web-ui-handler';
import errorHandler from './helpers/error-handler';
import './config/database';
import './config/orm';
import {
createBullBoardHandler,
serverAdapter,
@@ -21,7 +20,6 @@ if (appConfig.appEnv === 'development') {
}
const app = express();
const port = appConfig.port;
if (appConfig.appEnv === 'development') {
injectBullBoardHandler(app, serverAdapter);
@@ -44,6 +42,4 @@ app.use(function (req: Request, res: Response, next: NextFunction) {
app.use(errorHandler);
app.listen(port, () => {
logger.info(`Server is listening on ${port}`);
});
export default app;

View File

@@ -5,15 +5,16 @@ type AppConfig = {
host: string;
protocol: string;
port: string;
webAppUrl?: string;
webAppUrl: string;
appEnv: string;
isDev: boolean;
postgresDatabase: string;
postgresPort: number;
postgresHost: string;
postgresUsername: string;
postgresPassword: string;
postgresPassword?: string;
postgresEnableSsl: boolean;
baseUrl?: string;
baseUrl: string;
encryptionKey: string;
appSecretKey: string;
serveWebAppSeparately: boolean;
@@ -21,37 +22,44 @@ type AppConfig = {
redisPort: number;
};
const host = process.env.HOST || 'localhost';
const protocol = process.env.PROTOCOL || 'http';
const port = process.env.PORT || '3000';
const serveWebAppSeparately = process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let webAppUrl = `${protocol}://${host}:${port}`;
if (serveWebAppSeparately) {
webAppUrl = process.env.WEB_APP_URL || 'http://localhost:3001';
}
const baseUrl = `${protocol}://${host}:${port}`;
const appEnv = process.env.APP_ENV || 'development';
const appConfig: AppConfig = {
host: process.env.HOST || 'localhost',
protocol: process.env.PROTOCOL || 'http',
port: process.env.PORT || '3000',
appEnv: process.env.APP_ENV || 'development',
host,
protocol,
port,
appEnv: appEnv,
isDev: appEnv === 'development',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresPort: parseInt(process.env.POSTGRES_PORT) || 5432,
postgresPort: parseInt(process.env.POSTGRES_PORT|| '5432'),
postgresHost: process.env.POSTGRES_HOST || 'localhost',
postgresUsername:
process.env.POSTGRES_USERNAME || 'automatish_development_user',
process.env.POSTGRES_USERNAME || 'automatisch_development_user',
postgresPassword: process.env.POSTGRES_PASSWORD,
postgresEnableSsl: process.env.POSTGRES_ENABLE_SSL === 'true' ? true : false,
encryptionKey: process.env.ENCRYPTION_KEY,
appSecretKey: process.env.APP_SECRET_KEY,
serveWebAppSeparately:
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false,
encryptionKey: process.env.ENCRYPTION_KEY || '',
appSecretKey: process.env.APP_SECRET_KEY || '',
serveWebAppSeparately,
redisHost: process.env.REDIS_HOST || '127.0.0.1',
redisPort: parseInt(process.env.REDIS_PORT) || 6379,
redisPort: parseInt(process.env.REDIS_PORT || '6379'),
baseUrl,
webAppUrl,
};
if (appConfig.serveWebAppSeparately) {
appConfig.webAppUrl = process.env.WEB_APP_URL || 'http://localhost:3001';
} else {
appConfig.webAppUrl = `${appConfig.protocol}://${appConfig.host}:${appConfig.port}`;
}
if (!appConfig.encryptionKey) {
throw new Error('ENCRYPTION_KEY environment variable needs to be set!');
}
const baseUrl = `${appConfig.protocol}://${appConfig.host}:${appConfig.port}`;
appConfig.baseUrl = baseUrl;
export default appConfig;

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import appConfig from './app';
const redisConfig = {
host: appConfig.redisHost,
port: appConfig.redisPort,
enableOfflineQueue: false,
};
export default redisConfig;

View File

@@ -22,6 +22,8 @@ const createAuthData = async (
const appClass = (await import(`../../apps/${connection.key}`)).default;
const appData = App.findOneByKey(connection.key);
if (!connection.formattedData) { return null; }
const appInstance = new appClass(appData, {
consumerKey: connection.formattedData.consumerKey,
consumerSecret: connection.formattedData.consumerSecret,

View File

@@ -19,6 +19,8 @@ const deleteStep = async (
})
.throwIfNotFound();
if (!step) return;
await step.$query().delete();
const nextSteps = await step.flow

View File

@@ -18,6 +18,8 @@ const resetConnection = async (
})
.throwIfNotFound();
if (!connection.formattedData) { return null; }
connection = await connection.$query().patchAndFetch({
formattedData: { screenName: connection.formattedData.screenName },
});

View File

@@ -26,10 +26,13 @@ const getConnectedApps = async (
.filter((app: IApp) => connectionKeys.includes(app.key))
.map((app: IApp) => {
const connection = connections.find(
(connection: IConnection) => connection.key === app.key
(connection) => (connection as IConnection).key === app.key
);
app.connectionCount = connection.count;
if (connection) {
app.connectionCount = connection.count;
}
return app;
});

View File

@@ -12,8 +12,12 @@ const getData = async (_parent: unknown, params: Params, context: Context) => {
.withGraphFetched('connection')
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!connection || !step.appKey) return null;
const appData = App.findOneByKey(step.appKey);
const AppClass = (await import(`../../apps/${step.appKey}`)).default;

View File

@@ -10,7 +10,7 @@ const levels = {
}
const level = () => {
return appConfig.appEnv === 'development' ? 'debug' : 'warn'
return appConfig.appEnv === 'development' ? 'debug' : 'info'
}
const colors = {

View File

@@ -1,5 +1,6 @@
import fs from 'fs';
import { dirname, join } from 'path';
import { IApp } from '@automatisch/types';
import appInfoConverter from '../helpers/app-info-converter';
class App {
@@ -7,7 +8,7 @@ class App {
static folderPath = join(dirname(this.backendPath), 'apps');
static list = fs.readdirSync(this.folderPath);
static findAll(name?: string): object[] {
static findAll(name?: string): IApp[] {
if (!name) return this.list.map((name) => this.findOneByName(name));
return this.list
@@ -15,7 +16,7 @@ class App {
.map((name) => this.findOneByName(name));
}
static findOneByName(name: string): object {
static findOneByName(name: string): IApp {
const rawAppData = fs.readFileSync(
this.folderPath + `/${name}/info.json`,
'utf-8'
@@ -23,7 +24,7 @@ class App {
return appInfoConverter(rawAppData);
}
static findOneByKey(key: string): object {
static findOneByKey(key: string): IApp {
const rawAppData = fs.readFileSync(
this.folderPath + `/${key}/info.json`,
'utf-8'

View File

@@ -9,10 +9,10 @@ import { IJSONObject } from '@automatisch/types';
class Connection extends Base {
id!: string;
key!: string;
data: string;
formattedData!: IJSONObject;
data = '';
formattedData?: IJSONObject;
userId!: string;
verified: boolean;
verified = false;
count: number;
static tableName = 'connections';
@@ -50,7 +50,7 @@ class Connection extends Base {
appConfig.encryptionKey
).toString();
delete this['formattedData'];
delete this.formattedData;
}
decryptData(): void {

View File

@@ -8,7 +8,7 @@ class ExecutionStep extends Base {
stepId!: string;
dataIn!: Record<string, unknown>;
dataOut!: Record<string, unknown>;
status: string;
status = 'failure';
step: Step;
static tableName = 'execution_steps';

View File

@@ -5,8 +5,8 @@ import ExecutionStep from './execution-step';
class Execution extends Base {
id!: string;
flowId!: string;
testRun: boolean;
executionSteps: ExecutionStep[];
testRun = false;
executionSteps: ExecutionStep[] = [];
static tableName = 'executions';

View File

@@ -6,9 +6,9 @@ import Execution from './execution';
class Flow extends Base {
id!: string;
name: string;
name!: string;
userId!: string;
active: boolean;
active = false;
steps?: [Step];
static tableName = 'flows';

View File

@@ -7,15 +7,15 @@ import type { IStep } from '@automatisch/types';
class Step extends Base {
id!: string;
flowId!: string;
key: string;
appKey: string;
key?: string;
appKey?: string;
type!: IStep["type"];
connectionId?: string;
status: string;
position: number;
parameters: Record<string, unknown>;
status = 'incomplete';
position!: number;
parameters: Record<string, unknown> = {};
connection?: Connection;
flow?: Flow;
flow: Flow;
executionSteps?: [ExecutionStep];
static tableName = 'steps';

View File

@@ -1,11 +1,22 @@
import process from 'process';
import { Queue, QueueScheduler } from 'bullmq';
import redisConfig from '../config/redis';
import logger from '../helpers/logger';
const CONNECTION_REFUSED = 'ECONNREFUSED';
const redisConnection = {
connection: redisConfig,
};
new QueueScheduler('processor', redisConnection);
const processorQueue = new Queue('processor', redisConnection);
new QueueScheduler('processor', redisConnection);
processorQueue.on('error', (err) => {
if ((err as any).code === CONNECTION_REFUSED) {
logger.error('Make sure you have installed Redis and it is running.', err);
process.exit();
}
});
export default processorQueue;

View File

@@ -0,0 +1,9 @@
import app from './app';
import appConfig from './config/app';
import logger from './helpers/logger';
const port = appConfig.port;
app.listen(port, () => {
logger.info(`Server is listening on ${port}`);
});

View File

@@ -14,8 +14,8 @@ type ProcessorOptions = {
class Processor {
flow: Flow;
untilStep: Step;
testRun: boolean;
untilStep?: Step;
testRun?: boolean;
static variableRegExp = /({{step\..+\..+}})/g;
@@ -32,7 +32,8 @@ class Processor {
.orderBy('position', 'asc');
const triggerStep = steps.find((step) => step.type === 'trigger');
let initialTriggerData = await this.getInitialTriggerData(triggerStep);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let initialTriggerData = await this.getInitialTriggerData(triggerStep!);
if (this.testRun) {
initialTriggerData = [initialTriggerData[0]];
@@ -53,6 +54,8 @@ class Processor {
let fetchedActionData = {};
for await (const step of steps) {
if (!step.appKey) continue;
const appData = App.findOneByKey(step.appKey);
const {
@@ -74,11 +77,11 @@ class Processor {
const appInstance = new AppClass(
appData,
connection.formattedData,
connection?.formattedData,
computedParameters
);
if (!isTrigger) {
if (!isTrigger && key) {
const command = appInstance.actions[key];
fetchedActionData = await command.run();
}
@@ -107,17 +110,21 @@ class Processor {
.orderBy('created_at', 'desc')
.first();
return lastExecutionStepFromFirstExecution.dataOut;
return lastExecutionStepFromFirstExecution?.dataOut;
}
async getInitialTriggerData(step: Step) {
if (!step.appKey) return null;
const appData = App.findOneByKey(step.appKey);
const { appKey, connection, key, parameters: rawParameters = {} } = step;
if (!key) return null;
const AppClass = (await import(`../apps/${appKey}`)).default;
const appInstance = new AppClass(
appData,
connection.formattedData,
connection?.formattedData,
rawParameters
);
@@ -149,30 +156,34 @@ class Processor {
executionSteps: ExecutionSteps
): Step['parameters'] {
const entries = Object.entries(parameters);
return entries.reduce((result, [key, value]: [string, string]) => {
const parts = value.split(Processor.variableRegExp);
return entries.reduce((result, [key, value]: [string, unknown]) => {
if (typeof value === 'string') {
const parts = value.split(Processor.variableRegExp);
const computedValue = parts
.map((part: string) => {
const isVariable = part.match(Processor.variableRegExp);
if (isVariable) {
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '') as string;
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
const keyPath = keyPaths.join('.');
const executionStep = executionSteps[stepId.toString() as string];
const data = executionStep?.dataOut;
const dataValue = get(data, keyPath);
return dataValue;
}
const computedValue = parts
.map((part: string) => {
const isVariable = part.match(Processor.variableRegExp);
if (isVariable) {
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '') as string;
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
const keyPath = keyPaths.join('.');
const executionStep = executionSteps[stepId.toString() as string];
const data = executionStep?.dataOut;
const dataValue = get(data, keyPath);
return dataValue;
}
return part;
})
.join('');
return part;
})
.join('');
return {
...result,
[key]: computedValue,
};
return {
...result,
[key]: computedValue,
};
}
return result;
}, {});
}
}

View File

@@ -1,10 +1,9 @@
import type { IApp } from '@automatisch/types';
import JSONObject from './json-object';
import type { IApp, IJSONObject } from '@automatisch/types';
export default interface AuthenticationInterface {
appData: IApp;
connectionData: IJSONObject;
client: unknown;
verifyCredentials(): Promise<JSONObject>;
verifyCredentials(): Promise<IJSONObject>;
isStillVerified(): Promise<boolean>;
}

View File

@@ -1,2 +1,2 @@
import './config/database';
import './config/orm';
import './workers/processor';