fix(core): Moved logic from app to core.

This commit is contained in:
Rodrigo Rodriguez (pragmatismo.io) 2018-11-26 15:54:34 -02:00
parent 09715bcfc0
commit c1db8be0c0
6 changed files with 221 additions and 199 deletions

View file

@ -32,31 +32,21 @@
'use strict'; 'use strict';
import { BotAdapter } from 'botbuilder';
import { GBError } from 'botlib';
import { IGBPackage } from 'botlib';
import * as fs from 'fs';
import { Messages } from '../strings';
const logger = require('../../../src/logger');
import { WaterfallDialog } from 'botbuilder-dialogs'; import { WaterfallDialog } from 'botbuilder-dialogs';
import { IGBCoreService, IGBInstance } from 'botlib'; import { IGBInstance, IGBPackage } from 'botlib';
import { resolve } from 'bluebird';
const util = require('util');
const vm = require('vm');
/** /**
* @fileoverview General Bots server core. * @fileoverview General Bots server core.
*/ */
export class DialogClass { export class DialogClass {
public step: any;
public min: IGBInstance; public min: IGBInstance;
constructor(min: IGBInstance) { constructor(min: IGBInstance) {
this.min = min; this.min = min;
} }
public async expectMessage(text: string): Promise<string> { public async expectMessage(text: string): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.min.dialogs.add( this.min.dialogs.add(
new WaterfallDialog('/vmExpect', [ new WaterfallDialog('/vmExpect', [
@ -67,8 +57,8 @@ export class DialogClass {
async step => { async step => {
resolve(step.result); resolve(step.result);
return await step.next(); return await step.next();
}, }
]), ])
); );
}); });
} }
@ -79,10 +69,8 @@ export class DialogClass {
async step => { async step => {
await step.context.sendActivity(text); await step.context.sendActivity(text);
return await step.next(); return await step.next();
}, }
]), ])
); );
} }
} }

View file

@ -36,14 +36,23 @@
'use strict'; 'use strict';
import { IGBCoreService, IGBInstance } from 'botlib'; import { IGBCoreService, IGBInstance, IGBPackage } from 'botlib';
import * as fs from 'fs'; import * as fs from 'fs';
import processExists = require('process-exists');
import { Sequelize } from 'sequelize-typescript'; import { Sequelize } from 'sequelize-typescript';
const logger = require('../../../src/logger');
import { GBAdminService } from '../../admin.gbapp/services/GBAdminService'; import { GBAdminService } from '../../admin.gbapp/services/GBAdminService';
import { GuaribasInstance } from '../models/GBModel'; import { GuaribasInstance } from '../models/GBModel';
import { GBConfigService } from './GBConfigService'; import { GBConfigService } from './GBConfigService';
import { AzureDeployerService } from 'packages/azuredeployer.gbapp/services/AzureDeployerService';
import { GBAnalyticsPackage } from 'packages/analytics.gblib';
import { GBAdminPackage } from 'packages/admin.gbapp/index';
import { GBCorePackage } from 'packages/core.gbapp';
import { GBCustomerSatisfactionPackage } from 'packages/customer-satisfaction.gbapp';
import { GBKBPackage } from 'packages/kb.gbapp';
import { GBSecurityPackage } from 'packages/security.gblib';
import { GBWhatsappPackage } from 'packages/whatsapp.gblib/index';
const logger = require('../../../src/logger');
const opn = require('opn');
/** /**
* Core service layer. * Core service layer.
@ -70,7 +79,7 @@ export class GBCoreService implements IGBCoreService {
private createTableQuery: ( private createTableQuery: (
tableName: string, tableName: string,
attributes: any, attributes: any,
options: any options: any,
) => string; ) => string;
/** /**
@ -136,15 +145,15 @@ export class GBCoreService implements IGBCoreService {
dialect: this.dialect, dialect: this.dialect,
storage: storage, storage: storage,
dialectOptions: { dialectOptions: {
encrypt: encrypt encrypt: encrypt,
}, },
pool: { pool: {
max: 32, max: 32,
min: 8, min: 8,
idle: 40000, idle: 40000,
evict: 40000, evict: 40000,
acquire: 40000 acquire: 40000,
} },
}); });
if (this.dialect === 'mssql') { if (this.dialect === 'mssql') {
@ -153,7 +162,7 @@ export class GBCoreService implements IGBCoreService {
this.queryGenerator.createTableQuery = ( this.queryGenerator.createTableQuery = (
tableName, tableName,
attributes, attributes,
options options,
) => this.createTableQueryOverride(tableName, attributes, options); ) => this.createTableQueryOverride(tableName, attributes, options);
this.changeColumnQuery = this.queryGenerator.changeColumnQuery; this.changeColumnQuery = this.queryGenerator.changeColumnQuery;
this.queryGenerator.changeColumnQuery = (tableName, attributes) => this.queryGenerator.changeColumnQuery = (tableName, attributes) =>
@ -163,7 +172,7 @@ export class GBCoreService implements IGBCoreService {
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
} },
); );
} }
@ -174,7 +183,7 @@ export class GBCoreService implements IGBCoreService {
logger.info('Syncing database...'); logger.info('Syncing database...');
return this.sequelize.sync({ return this.sequelize.sync({
alter: alter, alter: alter,
force: force force: force,
}); });
} else { } else {
const msg = 'Database synchronization is disabled.'; const msg = 'Database synchronization is disabled.';
@ -257,7 +266,7 @@ export class GBCoreService implements IGBCoreService {
let sql: string = this.createTableQuery.apply(this.queryGenerator, [ let sql: string = this.createTableQuery.apply(this.queryGenerator, [
tableName, tableName,
attributes, attributes,
options options,
]); ]);
const re1 = /CREATE\s+TABLE\s+\[([^\]]*)\]/; const re1 = /CREATE\s+TABLE\s+\[([^\]]*)\]/;
const matches = re1.exec(sql); const matches = re1.exec(sql);
@ -268,7 +277,7 @@ export class GBCoreService implements IGBCoreService {
re2, re2,
(match: string, ...args: any[]): string => { (match: string, ...args: any[]): string => {
return 'CONSTRAINT [' + table + '_pk] ' + match; return 'CONSTRAINT [' + table + '_pk] ' + match;
} },
); );
const re3 = /FOREIGN\s+KEY\s+\((\[[^\]]*\](?:,\s*\[[^\]]*\])*)\)/g; const re3 = /FOREIGN\s+KEY\s+\((\[[^\]]*\](?:,\s*\[[^\]]*\])*)\)/g;
const re4 = /\[([^\]]*)\]/g; const re4 = /\[([^\]]*)\]/g;
@ -283,7 +292,7 @@ export class GBCoreService implements IGBCoreService {
matches = re4.exec(fkcols); matches = re4.exec(fkcols);
} }
return 'CONSTRAINT [' + fkname + '_fk] FOREIGN KEY (' + fkcols + ')'; return 'CONSTRAINT [' + fkname + '_fk] FOREIGN KEY (' + fkcols + ')';
} },
); );
} }
return sql; return sql;
@ -300,7 +309,7 @@ export class GBCoreService implements IGBCoreService {
private changeColumnQueryOverride(tableName, attributes): string { private changeColumnQueryOverride(tableName, attributes): string {
let sql: string = this.changeColumnQuery.apply(this.queryGenerator, [ let sql: string = this.changeColumnQuery.apply(this.queryGenerator, [
tableName, tableName,
attributes attributes,
]); ]);
const re1 = /ALTER\s+TABLE\s+\[([^\]]*)\]/; const re1 = /ALTER\s+TABLE\s+\[([^\]]*)\]/;
const matches = re1.exec(sql); const matches = re1.exec(sql);
@ -326,9 +335,151 @@ export class GBCoreService implements IGBCoreService {
fkcols + fkcols +
')' ')'
); );
} },
); );
} }
return sql; return sql;
} }
/**
* Loads all bot instances from object storage, if it's formatted.
*
* @param core
* @param azureDeployer
* @param proxyAddress
*/
public async loadAllInstances(
core: GBCoreService,
azureDeployer: AzureDeployerService,
proxyAddress: string,
) {
logger.info(`Loading instances from storage...`);
let instances: GuaribasInstance[];
try {
instances = await core.loadInstances();
const instance = instances[0];
if (process.env.NODE_ENV === 'development') {
logger.info(`Updating bot endpoint to local reverse proxy (ngrok)...`);
await azureDeployer.updateBotProxy(
instance.botId,
instance.botId,
`${proxyAddress}/api/messages/${instance.botId}`,
);
}
} catch (error) {
if (error.parent.code === 'ELOGIN') {
const group = GBConfigService.get('CLOUD_GROUP');
const serverName = GBConfigService.get('STORAGE_SERVER').split(
'.database.windows.net',
)[0];
await azureDeployer.openStorageFirewall(group, serverName);
} else {
// Check if storage is empty and needs formatting.
const isInvalidObject =
error.parent.number == 208 || error.parent.errno == 1; // MSSQL or SQLITE.
if (isInvalidObject) {
if (GBConfigService.get('STORAGE_SYNC') != 'true') {
throw new Error(
`Operating storage is out of sync or there is a storage connection error. Try setting STORAGE_SYNC to true in .env file. Error: ${
error.message
}.`,
);
} else {
logger.info(
`Storage is empty. After collecting storage structure from all .gbapps it will get synced.`,
);
}
} else {
throw new Error(
`Cannot connect to operating storage: ${error.message}.`,
);
}
}
}
return instances;
}
/**
* If instances is undefined here it's because storage has been formatted.
* Load all instances from .gbot found on deploy package directory.
* @param instances
* @param bootInstance
* @param core
*/
public async ensureInstances(
instances: GuaribasInstance[],
bootInstance: any,
core: GBCoreService,
) {
if (!instances) {
const saveInstance = new GuaribasInstance(bootInstance);
await saveInstance.save();
instances = await core.loadInstances();
}
return instances;
}
public loadSysPackages(core: GBCoreService) {
// NOTE: if there is any code before this line a semicolon
// will be necessary before this line.
// Loads all system packages.
[
GBAdminPackage,
GBAnalyticsPackage,
GBCorePackage,
GBSecurityPackage,
GBKBPackage,
GBCustomerSatisfactionPackage,
GBWhatsappPackage,
].forEach(e => {
logger.info(`Loading sys package: ${e.name}...`);
const p = Object.create(e.prototype) as IGBPackage;
p.loadPackage(core, core.sequelize);
});
}
public ensureAdminIsSecured() {
const password = GBConfigService.get('ADMIN_PASS');
if (!GBAdminService.StrongRegex.test(password)) {
throw new Error(
'Please, define a really strong password in ADMIN_PASS environment variable before running the server.',
);
}
}
public async createBootInstance(
core: GBCoreService,
azureDeployer: AzureDeployerService,
proxyAddress: string,
) {
let bootInstance: IGBInstance;
try {
await core.initDatabase();
} catch (error) {
logger.info(
`Deploying cognitive infrastructure (on the cloud / on premises)...`,
);
try {
bootInstance = await azureDeployer.deployFarm(proxyAddress);
} catch (error) {
logger.warn(
'In case of error, please cleanup any infrastructure objects created during this procedure and .env before running again.',
);
throw error;
}
core.writeEnv(bootInstance);
logger.info(`File .env written, starting General Bots...`);
GBConfigService.init();
await core.initDatabase();
}
return bootInstance;
}
public openBrowserInDevelopment() {
if (process.env.NODE_ENV === 'development') {
opn('http://localhost:4242');
}
}
} }

View file

@ -52,6 +52,7 @@ import { GuaribasInstance, GuaribasPackage } from '../models/GBModel';
import { KBService } from './../../kb.gbapp/services/KBService'; import { KBService } from './../../kb.gbapp/services/KBService';
import { GBConfigService } from './GBConfigService'; import { GBConfigService } from './GBConfigService';
import { GBImporter } from './GBImporterService'; import { GBImporter } from './GBImporterService';
import { GBVMService } from './GBVMService';
/** Deployer service for bots, themes, ai and more. */ /** Deployer service for bots, themes, ai and more. */
export class GBDeployer { export class GBDeployer {
@ -266,6 +267,10 @@ export class GBDeployer {
}); });
} }
public deployScriptToStorage(instanceId: number, localPath: string) {
}
public deployTheme(localPath: string) { public deployTheme(localPath: string) {
// DISABLED: Until completed, "/ui/public". // DISABLED: Until completed, "/ui/public".
// FsExtra.copy(localPath, this.workDir + packageName) // FsExtra.copy(localPath, this.workDir + packageName)
@ -297,14 +302,12 @@ export class GBDeployer {
break; break;
case '.gbdialog': case '.gbdialog':
const vm = new VMService(this.core.sequelize); const vm = new GBVMService();
return service.deployKb(this.core, this, localPath); return service.deployKb(this.core, this, localPath);
break;
default: default:
const err = GBError.create( const err = GBError.create(
`GuaribasBusinessError: Unknow package type: ${packageType}.`, `GuaribasBusinessError: Unknown package type: ${packageType}.`
); );
Promise.reject(err); Promise.reject(err);
break; break;

View file

@ -36,13 +36,17 @@ import { IGBCoreService, IGBInstance } from 'botlib';
import { GBError } from 'botlib'; import { GBError } from 'botlib';
import { IGBPackage } from 'botlib'; import { IGBPackage } from 'botlib';
const logger = require('../../../src/logger'); const logger = require('../../../src/logger');
import * as fs from 'fs';
import { BotAdapter } from 'botbuilder'; import { BotAdapter } from 'botbuilder';
import { WaterfallDialog } from 'botbuilder-dialogs'; import { WaterfallDialog } from 'botbuilder-dialogs';
import * as fs from 'fs';
import { Messages } from '../strings'; import { Messages } from '../strings';
import { DialogClass } from './GBAPIService';
import { GBDeployer } from './GBDeployer'; import { GBDeployer } from './GBDeployer';
const util = require('util'); const util = require('util');
const vm = require('vm'); const vm = require('vm');
import processExists = require('process-exists');
import { Sequelize } from 'sequelize-typescript';
const UrlJoin = require('url-join');
/** /**
* @fileoverview General Bots server core. * @fileoverview General Bots server core.
@ -50,50 +54,27 @@ const vm = require('vm');
export class GBVMService implements IGBCoreService { export class GBVMService implements IGBCoreService {
public static setup(bot: BotAdapter, min: IGBInstance) { private script = new vm.Script();
} public async loadJS(
public loadJS(
filename: string, filename: string,
min: IGBInstance, min: IGBInstance,
core: IGBCoreService, core: IGBCoreService,
deployer: GBDeployer, deployer: GBDeployer,
localPath: string localPath: string
) { ): Promise<void> {
const sandbox = { const code = fs.readFileSync(UrlJoin(localPath, filename), 'utf8');
animal: 'cat', const sandbox = new DialogClass(min);
count: 2,
};
const script = new vm.Script('count += 1; name = "kitty";');
const context = vm.createContext(sandbox); const context = vm.createContext(sandbox);
this.script.runInContext(context);
for (let i = 0; i < 10; ++i) {
script.runInContext(context);
}
console.log(util.inspect(sandbox)); console.log(util.inspect(sandbox));
// { animal: 'cat', count: 12, name: 'kitty' } await deployer.deployScriptToStorage(
min.instanceId,
const packageType = Path.extname(localPath); filename
const packageName = Path.basename(localPath);
logger.info(`[GBDeployer] Opening package: ${localPath}`);
const packageObject = JSON.parse(
Fs.readFileSync(UrlJoin(localPath, 'package.json'), 'utf8'),
); );
logger.info(`[GBVMService] Finished loading of ${filename}`);
const instance = await core.loadInstance(packageObject.botId);
logger.info(`[GBDeployer] Importing: ${localPath}`);
const p = await deployer.deployPackageToStorage(
instance.instanceId,
packageName,
);
await this.importKbPackage(localPath, p, instance);
deployer.rebuildIndex(instance);
logger.info(`[GBDeployer] Finished import of ${localPath}`);
} }
} }

View file

@ -43,7 +43,7 @@ describe('Load function', () => {
it('should fail on invalid file', () => { it('should fail on invalid file', () => {
try { try {
const service = new GBVMService(); const service = new GBVMService();
service.loadJS('invalid.file'); service.loadJS('invalid.file', null, null, null, null);
} catch (error) { } catch (error) {
expect(error).to.equal(0); expect(error).to.equal(0);
} }

View file

@ -40,14 +40,9 @@
const logger = require('./logger'); const logger = require('./logger');
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const opn = require('opn');
import { IGBInstance, IGBPackage } from 'botlib'; import { IGBInstance, IGBPackage } from 'botlib';
import { GBAdminPackage } from '../packages/admin.gbapp/index';
import { GBAdminService } from '../packages/admin.gbapp/services/GBAdminService'; import { GBAdminService } from '../packages/admin.gbapp/services/GBAdminService';
import { GBAnalyticsPackage } from '../packages/analytics.gblib';
import { AzureDeployerService } from '../packages/azuredeployer.gbapp/services/AzureDeployerService'; import { AzureDeployerService } from '../packages/azuredeployer.gbapp/services/AzureDeployerService';
import { GBCorePackage } from '../packages/core.gbapp';
import { GuaribasInstance } from '../packages/core.gbapp/models/GBModel'; import { GuaribasInstance } from '../packages/core.gbapp/models/GBModel';
import { GBConfigService } from '../packages/core.gbapp/services/GBConfigService'; import { GBConfigService } from '../packages/core.gbapp/services/GBConfigService';
import { GBConversationalService } from '../packages/core.gbapp/services/GBConversationalService'; import { GBConversationalService } from '../packages/core.gbapp/services/GBConversationalService';
@ -55,10 +50,6 @@ import { GBCoreService } from '../packages/core.gbapp/services/GBCoreService';
import { GBDeployer } from '../packages/core.gbapp/services/GBDeployer'; import { GBDeployer } from '../packages/core.gbapp/services/GBDeployer';
import { GBImporter } from '../packages/core.gbapp/services/GBImporterService'; import { GBImporter } from '../packages/core.gbapp/services/GBImporterService';
import { GBMinService } from '../packages/core.gbapp/services/GBMinService'; import { GBMinService } from '../packages/core.gbapp/services/GBMinService';
import { GBCustomerSatisfactionPackage } from '../packages/customer-satisfaction.gbapp';
import { GBKBPackage } from '../packages/kb.gbapp';
import { GBSecurityPackage } from '../packages/security.gblib';
import { GBWhatsappPackage } from './../packages/whatsapp.gblib/index';
const appPackages = new Array<IGBPackage>(); const appPackages = new Array<IGBPackage>();
@ -66,13 +57,11 @@ const appPackages = new Array<IGBPackage>();
* General Bots open-core entry point. * General Bots open-core entry point.
*/ */
export class GBServer { export class GBServer {
/** /**
* Program entry-point. * Program entry-point.
*/ */
public static run() { public static run() {
logger.info(`The Bot Server is in STARTING mode...`); logger.info(`The Bot Server is in STARTING mode...`);
// Creates a basic HTTP server that will serve several URL, one for each // Creates a basic HTTP server that will serve several URL, one for each
@ -86,8 +75,8 @@ export class GBServer {
server.use( server.use(
bodyParser.urlencoded({ bodyParser.urlencoded({
// to support URL-encoded bodies // to support URL-encoded bodies
extended: true extended: true,
}) }),
); );
let bootInstance: IGBInstance; let bootInstance: IGBInstance;
@ -106,133 +95,42 @@ export class GBServer {
logger.info(`Establishing a development local proxy (ngrok)...`); logger.info(`Establishing a development local proxy (ngrok)...`);
const proxyAddress = await core.ensureProxy(port); const proxyAddress = await core.ensureProxy(port);
logger.info(`Deploying packages...`);
const deployer = new GBDeployer(core, new GBImporter(core)); const deployer = new GBDeployer(core, new GBImporter(core));
const azureDeployer = new AzureDeployerService(deployer); const azureDeployer = new AzureDeployerService(deployer);
try {
await core.initDatabase();
} catch (error) {
logger.info(`Deploying cognitive infrastructure (on the cloud / on premises)...`);
try {
bootInstance = await azureDeployer.deployFarm(proxyAddress);
} catch (error) {
logger.warn(
'In case of error, please cleanup any infrastructure objects created during this procedure and .env before running again.'
);
throw error;
}
core.writeEnv(bootInstance);
logger.info(`File .env written, starting General Bots...`);
GBConfigService.init();
await core.initDatabase();
}
// TODO: Get .gb* templates from GitHub and download do additional deploy folder.
// Check admin password.
const conversationalService = new GBConversationalService(core);
const adminService = new GBAdminService(core); const adminService = new GBAdminService(core);
const password = GBConfigService.get('ADMIN_PASS'); const conversationalService = new GBConversationalService(core);
bootInstance = await core.createBootInstance(
if (!GBAdminService.StrongRegex.test(password)) { core,
throw new Error( azureDeployer,
'Please, define a really strong password in ADMIN_PASS environment variable before running the server.' proxyAddress,
); );
} core.ensureAdminIsSecured();
core.loadSysPackages(core);
// NOTE: the semicolon is necessary before this line.
// Loads all system packages.
[
GBAdminPackage,
GBAnalyticsPackage,
GBCorePackage,
GBSecurityPackage,
GBKBPackage,
GBCustomerSatisfactionPackage,
GBWhatsappPackage
].forEach(e => {
logger.info(`Loading sys package: ${e.name}...`);
const p = Object.create(e.prototype) as IGBPackage;
p.loadPackage(core, core.sequelize);
});
// Loads all bot instances from object storage, if it's formatted.
logger.info(`Loading instances from storage...`);
let instances: GuaribasInstance[];
try {
instances = await core.loadInstances();
const instance = instances[0];
if (process.env.NODE_ENV === 'development') {
logger.info(`Updating bot endpoint to local reverse proxy (ngrok)...`);
await azureDeployer.updateBotProxy(
instance.botId,
instance.botId,
`${proxyAddress}/api/messages/${instance.botId}`
);
}
} catch (error) {
if (error.parent.code === 'ELOGIN') {
const group = GBConfigService.get('CLOUD_GROUP');
const serverName = GBConfigService.get('STORAGE_SERVER').split(
'.database.windows.net'
)[0];
await azureDeployer.openStorageFirewall(group, serverName);
} else {
// Check if storage is empty and needs formatting.
const isInvalidObject =
error.parent.number == 208 || error.parent.errno == 1; // MSSQL or SQLITE.
if (isInvalidObject) {
if (GBConfigService.get('STORAGE_SYNC') != 'true') {
throw new Error(`Operating storage is out of sync or there is a storage connection error. Try setting STORAGE_SYNC to true in .env file. Error: ${
error.message
}.`);
} else {
logger.info(
`Storage is empty. After collecting storage structure from all .gbapps it will get synced.`
);
}
} else {
throw new Error(`Cannot connect to operating storage: ${error.message}.`);
}
}
}
// Deploy packages and format object store according to .gbapp storage models.
logger.info(`Deploying packages...`);
await deployer.deployPackages(core, server, appPackages); await deployer.deployPackages(core, server, appPackages);
// If instances is undefined here it's because storage has been formatted.
// Load all instances from .gbot found on deploy package directory.
if (!instances) {
const saveInstance = new GuaribasInstance(bootInstance);
await saveInstance.save();
instances = await core.loadInstances();
}
// Setup server dynamic (per bot instance) resources and listeners.
logger.info(`Publishing instances...`); logger.info(`Publishing instances...`);
let instances: GuaribasInstance[] = await core.loadAllInstances(
core,
azureDeployer,
proxyAddress,
);
instances = await core.ensureInstances(
instances,
bootInstance,
core
);
const minService = new GBMinService( const minService = new GBMinService(
core, core,
conversationalService, conversationalService,
adminService, adminService,
deployer deployer,
); );
await minService.buildMin(server, appPackages, instances); await minService.buildMin(server, appPackages, instances);
logger.info(`The Bot Server is in RUNNING mode...`);
if (process.env.NODE_ENV === 'development') { logger.info(`The Bot Server is in RUNNING mode...`);
opn('http://localhost:4242'); core.openBrowserInDevelopment();
}
return core; return core;
} catch (err) { } catch (err) {
@ -244,6 +142,7 @@ export class GBServer {
} }
} }
// First line to run. // First line to run.
GBServer.run(); GBServer.run();