diff --git a/api/8s9Auqlc7RHbpxsJ/index.js b/api/8s9Auqlc7RHbpxsJ/index.js new file mode 100644 index 00000000..c1558ae7 --- /dev/null +++ b/api/8s9Auqlc7RHbpxsJ/index.js @@ -0,0 +1,80 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; + + +/** + * api invoke_trigger + */ + +class _8s9Auqlc7RHbpxsJ extends Endpoint { + constructor() { + super('8s9Auqlc7RHbpxsJ'); + } + + /** + * invokes a trigger using provided trigger name + * @param app + * @param req (p0: trigger_name) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let triggerName; + + if (!req.body.p0) { + return res.status(400).send({ + status : 'fail', + message: 'p0 is required' + }); + } + else { + triggerName = req.body.p0; + + } + + try { + console.log(`[api ${this.endpoint}] Received request to invoke trigger ${triggerName}. Checking if the trigger exists`); + + if (!(triggerEngine.checkTriggerExists(triggerName))) { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger does not exist' + }); + } + + if (triggerEngine.isTriggerDisabled(triggerName)) { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} cannot be invoked as it is disabled`); + return res.send({ + status : 'fail', + message: 'trigger is disabled' + }); + } + + return triggerEngine.invokeTrigger(triggerName) + .then(_ => { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} successfully invoked`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Failed to invoke trigger ${triggerName}. Error: ${err}`); + return res.send({ + status : 'fail', + message: `Failed to invoke trigger: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] Error while invoking trigger ${triggerName}: ${e}`); + return res.send({ + status : 'fail', + message: 'invoke_trigger_error' + }); + } + } +} + + +export default new _8s9Auqlc7RHbpxsJ(); diff --git a/api/GxFK1FzbVyWUx8MR/index.js b/api/GxFK1FzbVyWUx8MR/index.js new file mode 100644 index 00000000..2c5993a4 --- /dev/null +++ b/api/GxFK1FzbVyWUx8MR/index.js @@ -0,0 +1,95 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; +import {validateTriggerAction} from '../../trigger/validation'; + + +/** + * api add_trigger_action + */ + +class _GxFK1FzbVyWUx8MR extends Endpoint { + constructor() { + super('GxFK1FzbVyWUx8MR'); + } + + /** + * Adds a trigger action for an existing trigger + * @param app + * @param req (p0: trigger_name, p1: trigger_action) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let triggerName; + let triggerAction; + + if (!req.body.p0 || !req.body.p1) { + return res.status(400).send({ + status : 'fail', + message: 'p0 and p1 are required' + }); + } + else { + triggerName = req.body.p0; + triggerAction = req.body.p1; + } + + try { + console.log(`[api ${this.endpoint}] Received request to add triger action ${triggerAction.name} to trigger ${triggerName}. Validating trigger action`); + + try { + validateTriggerAction(triggerAction); + } catch(err) { + console.log(`[api ${this.endpoint}] Trigger action is invalid`); + return res.send({ + status : 'fail', + message: 'trigger action is invalid' + }); + } + + console.log(`[api ${this.endpoint}] Checking if the trigger exists`); + + if (!(triggerEngine.checkTriggerExists(triggerName))) { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger does not exist' + }); + } + + console.log(`[api ${this.endpoint}] Checking if trigger action ${triggerAction.name} exists`); + if (triggerEngine.checkActionExists(triggerName, triggerAction.name)) { + console.log(`[api ${this.endpoint}] Trigger action ${triggerAction.name} already exists`); + return res.send({ + status : 'fail', + message: 'trigger action already exists' + }); + } + + return triggerEngine.addTriggerAction(triggerName, triggerAction) + .then(_ => { + console.log(`[api ${this.endpoint}] Trigger action ${triggerAction.name} added. Returning success`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Error while adding trigger action ${triggerAction.name}: ${err}`); + return res.send({ + status : 'failure', + message: `Failed to add action: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] Error while adding trigger action: ${e}`); + return res.send({ + status : 'fail', + message: 'add_trigger_action_error' + }); + } + } +} + + +export default new _GxFK1FzbVyWUx8MR(); diff --git a/api/Hehn7N0KBVvuFQHf/index.js b/api/Hehn7N0KBVvuFQHf/index.js new file mode 100644 index 00000000..289c3678 --- /dev/null +++ b/api/Hehn7N0KBVvuFQHf/index.js @@ -0,0 +1,108 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; +import {validateTrigger, validateTriggerAction} from '../../trigger/validation'; + + +/** + * api add_trigger + */ + +class _Hehn7N0KBVvuFQHf extends Endpoint { + constructor() { + super('Hehn7N0KBVvuFQHf'); + } + + /** + * accepts a trigger and trigger actions payload to store a trigger and + * corresponding trigger actions to the database + * @param app + * @param req (p0: trigger, p1: trigger_actions) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let trigger; + let triggerActions; + + + if (!req.body.p0 || !req.body.p1) { + return res.status(400).send({ + status : 'fail', + message: 'p0 and p1 are required' + }); + } + else { + trigger = req.body.p0; + triggerActions = req.body.p1; + } + + try { + console.log(`[api ${this.endpoint}] Received request to add new trigger ${trigger.name}. Validating`); + + if (trigger.name === undefined) { + console.log(`[api ${this.endpoint} Missing trigger name`); + return res.send({ + status: 'failure', + message: 'Missing trigger name', + }); + } + + if (triggerEngine.checkTriggerExists(trigger.name)) { + console.log(`[api ${this.endpoint}] Trigger ${trigger.name} already exists`); + return res.send({ + status: 'failure', + message: 'Trigger already exists' + }); + } + + try { + validateTrigger(trigger); + } catch(err) { + console.log(`[api ${this.endpoint} Invalid trigger. Error: ${err}`); + return res.send({ + status: 'failure', + message: `Invalid trigger: ${err}`, + }); + } + + for (let triggerAction of triggerActions) { + try { + validateTriggerAction(triggerAction); + } catch(err) { + console.log(`[api ${this.endpoint}] Invalid trigger action ${triggerAction.name}. Error: ${err}`); + return res.send({ + status: 'failure', + message: `Invalid trigger action ${triggerAction.name}: ${err}` + }); + } + } + + console.log(`[api ${this.endpoint}] Validated trigger and trigger actions`); + + return triggerEngine.addTrigger(trigger, triggerActions) + .then(_ => { + console.log(`[api ${this.endpoint}] Trigger added. Returning success`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Error while adding trigger: ${err}`); + return res.send({ + status : 'fail', + message: `Failed to add trigger: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] error: ${e}`); + return res.send({ + status : 'fail', + message: 'add_trigger_error' + }); + } + } +} + + +export default new _Hehn7N0KBVvuFQHf(); diff --git a/api/RlUQHxrBmf1ryge3/index.js b/api/RlUQHxrBmf1ryge3/index.js new file mode 100644 index 00000000..f7db08f3 --- /dev/null +++ b/api/RlUQHxrBmf1ryge3/index.js @@ -0,0 +1,81 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; + + +/** + * api remove_trigger_action + */ + +class _RlUQHxrBmf1ryge3 extends Endpoint { + constructor() { + super('RlUQHxrBmf1ryge3'); + } + + /** + * removes a trigger action using provided trigger name and action + * @param app + * @param req (p0: trigger_name, p1required) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let triggerName; + let actionName; + + if (!req.body.p0 || !req.body.p1) { + return res.status(400).send({ + status : 'fail', + message: 'p0 and p1 are required' + }); + } + else { + triggerName = req.body.p0; + actionName = req.body.p1; + } + + try { + console.log(`[api ${this.endpoint}] Received request to remove trigger action ${actionName} for trigger ${triggerName}. Checking if trigger exists`); + + if (!(triggerEngine.checkTriggerExists(triggerName))) { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger does not exist' + }); + } + + if (!(triggerEngine.checkActionExists(triggerName, actionName))) { + console.log(`[api ${this.endpoint}] Trigger action ${actionName} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger action does not exist' + }); + } + + return triggerEngine.removeTriggerAction(triggerName, actionName) + .then(_ => { + console.log(`[api ${this.endpoint}] Trigger action ${actionName} removed for trigger ${triggerName}. Returning success`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Failed to remove trigger action ${actionName} for trigger ${triggerName}. Error: ${err}`); + return res.send({ + status : 'fail', + message: `Failed to remove trigger action: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] error: ${e}`); + return res.send({ + status : 'fail', + message: 'remove_trigger_action_error' + }); + } + } +} + + +export default new _RlUQHxrBmf1ryge3(); diff --git a/api/tLog09WkateEmT81/index.js b/api/tLog09WkateEmT81/index.js new file mode 100644 index 00000000..21952d80 --- /dev/null +++ b/api/tLog09WkateEmT81/index.js @@ -0,0 +1,96 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; + + +/** + * api update_trigger_action + */ + + +// Fields that canot be updated by the user +const unupdateableFields = ['id', 'trigger_id', 'last_action_message', 'last_action_date', 'create_date']; + + +class _tLog09WkateEmT81 extends Endpoint { + constructor() { + super('tLog09WkateEmT81'); + } + + /** + * updates a trigger action + * @param app + * @param req (p0: trigger_name, p1: trigger_action) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let triggerName; + let triggerAction; + + if (!req.body.p0 || !req.body.p1) { + return res.status(400).send({ + status : 'fail', + message: 'p0 and p1 are required' + }); + } + else { + triggerName = req.body.p0; + triggerAction = req.body.p1; + } + + try { + if (!(triggerAction.name)) { + console.log(`[api ${this.endpoint}] Missing trigger action name in the request`); + return res.send({ + status: 'fail', + message: 'trigger action name not sent' + }); + } + + console.log(`[api ${this.endpoint}] Received request to update trigger action ${triggerAction.name} for trigger ${triggerName}. Checking if trigger exists`); + + if (!(triggerEngine.checkTriggerExists(triggerName))) { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger does not exist' + }); + } + + for (let field of unupdateableFields) { + if (field in triggerAction) { + console.log(`[api ${this.endpoint}] Tried to update field ${field}. Rejecting`); + return res.send({ + status: 'fail', + message: 'field cannot be updated', + }); + } + } + + return triggerEngine.updateTriggerAction(triggerName, triggerAction) + .then(_ => { + console.log(`[api ${this.endpoint}] Updated trigger action ${triggerAction.name}`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Failed to update trigger action ${triggerAction.name}: ${err}`); + return res.send({ + status : 'fail', + message: `Failed to update trigger action: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] error: ${e}`); + return res.send({ + status : 'fail', + message: 'update_trigger_action_error' + }); + } + } +} + + +export default new _tLog09WkateEmT81(); diff --git a/api/vu35sd0ipsex2pDf/index.js b/api/vu35sd0ipsex2pDf/index.js new file mode 100644 index 00000000..1de45419 --- /dev/null +++ b/api/vu35sd0ipsex2pDf/index.js @@ -0,0 +1,71 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; + + +/** + * api remove_trigger + */ + +class _vu35sd0ipsex2pDf extends Endpoint { + constructor() { + super('vu35sd0ipsex2pDf'); + } + + /** + * removes a trigger using provided trigger name + * @param app + * @param req (p0: trigger_name) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let triggerName; + + if (!req.body.p0) { + return res.status(400).send({ + status : 'fail', + message: 'p0 is required' + }); + } + else { + triggerName = req.body.p0; + } + + try { + console.log(`[api ${this.endpoint}] Received request to remove trigger ${triggerName}. Checking if trigger exists`); + + if (!(triggerEngine.checkTriggerExists(triggerName))) { + console.log(`[api ${this.endpoint}] Trigger ${triggerName} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger does not exist' + }); + } + + return triggerEngine.removeTrigger(triggerName) + .then(_ => { + console.log(`[api ${this.endpoint}] Removed trigger ${triggerName}. Sending success`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Failed to remove trigger ${triggerName}: ${err}`); + return res.send({ + status : 'fail', + message: `Failed to remove trigger: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] error: ${e}`); + return res.send({ + status : 'fail', + message: 'remove_trigger_error' + }); + } + } +} + + +export default new _vu35sd0ipsex2pDf(); diff --git a/api/xnm05bThUrJjxkqT/index.js b/api/xnm05bThUrJjxkqT/index.js new file mode 100644 index 00000000..8d8c1bce --- /dev/null +++ b/api/xnm05bThUrJjxkqT/index.js @@ -0,0 +1,94 @@ +import Endpoint from '../endpoint'; +import triggerEngine from '../../trigger/trigger'; + + +/** + * api update_trigger + */ + + +// Fields that canot be updated by the user +const unupdateableFields = ['id', 'last_trigger_state', 'create_date']; + + +class _xnm05bThUrJjxkqT extends Endpoint { + constructor() { + super('xnm05bThUrJjxkqT'); + } + + /** + * updates a trigger + * @param app + * @param req (p0: trigger) + * @param res + * @returns {*} + */ + handler(app, req, res) { + let trigger; + + if (!req.body.p0) { + return res.status(400).send({ + status : 'fail', + message: 'p0 is required' + }); + } + else { + trigger = req.body.p0; + } + + try { + console.log(`[api ${this.endpoint}] Received request to update trigger ${trigger.name}. Checking if trigger exists`); + + if (!(trigger.name)) { + console.log(`[api ${this.endpoint}] Name not sent. Cannot proceed with update`); + return res.send({ + status : 'fail', + message: 'missing trigger name' + }); + } + + if (!(triggerEngine.checkTriggerExists(trigger.name))) { + console.log(`[api ${this.endpoint}] Trigger ${trigger.name} does not exist`); + return res.send({ + status : 'fail', + message: 'trigger does not exist' + }); + } + + for (let field of unupdateableFields) { + if (field in trigger) { + console.log(`[api ${this.endpoint}] Tried to update field ${field} of trigger ${trigger.name}. Rejecting`); + return res.send({ + status: 'fail', + message: 'field cannot be updated', + }); + } + } + + return triggerEngine.updateTrigger(trigger.name, trigger) + .then(_ => { + console.log(`[api ${this.endpoint}] Updated trigger ${trigger.name}. Sending success`); + return res.send({ + status: 'success' + }); + }) + .catch(err => { + console.log(`[api ${this.endpoint}] Failed to update trigger ${trigger.name}: ${err}`); + return res.send({ + status : 'fail', + message: `Failed to update trigger: ${err}` + }); + }); + } + catch (e) { + console.log(`[api ${this.endpoint}] error: ${e}`); + return res.send({ + status : 'fail', + message: 'update_trigger_error' + }); + } + } +} + + +export default new _xnm05bThUrJjxkqT(); diff --git a/core/config/config.js b/core/config/config.js index 1bdc6ae8..e2de26a3 100644 --- a/core/config/config.js +++ b/core/config/config.js @@ -230,7 +230,7 @@ if (DATABASE_ENGINE === 'sqlite') { DATABASE_CONNECTION.SCRIPT_INIT_MILLIX_JOB_ENGINE = './scripts/initialize-millix-job-engine-sqlite3.sql'; DATABASE_CONNECTION.SCRIPT_MIGRATION_DIR = './scripts/migration'; DATABASE_CONNECTION.SCRIPT_MIGRATION_SHARD_DIR = './scripts/migration/shard'; - DATABASE_CONNECTION.SCHEMA_VERSION = '8'; + DATABASE_CONNECTION.SCHEMA_VERSION = '9'; } export default { diff --git a/core/services/services.js b/core/services/services.js index e60aece4..70ec7bdb 100644 --- a/core/services/services.js +++ b/core/services/services.js @@ -6,9 +6,7 @@ import peerRotation from '../../net/peer-rotation'; import jobEngine from '../../job/job-engine'; import console from '../console'; import logManager from '../log-manager'; -import database from '../../database/database'; -import _ from 'lodash'; -import ntp from '../ntp'; +import triggerEngine from '../../trigger/trigger'; class Service { @@ -39,6 +37,7 @@ class Service { .then(() => jobEngine.initialize()) .then(() => wallet._doUpdateNodeAttribute()) .then(() => wallet._doTransactionOutputRefresh()) + .then(() => triggerEngine.initialize()) .catch(e => { console.log(e); }); diff --git a/database/database.js b/database/database.js index ee241384..56eb63bf 100644 --- a/database/database.js +++ b/database/database.js @@ -7,7 +7,7 @@ import wallet from '../core/wallet/wallet'; import console from '../core/console'; import path from 'path'; import async from 'async'; -import {Address, API, Config, Job, Keychain, Node, Schema, Shard as ShardRepository, Wallet} from './repositories/repositories'; +import {Address, API, Config, Job, Keychain, Node, Schema, Shard as ShardRepository, Wallet, Trigger} from './repositories/repositories'; import Shard from './shard'; import _ from 'lodash'; @@ -291,6 +291,7 @@ export class Database { this.repositories['address'] = new Address(this.databaseMillix); this.repositories['job'] = new Job(this.databaseJobEngine); this.repositories['api'] = new API(this.databaseMillix); + this.repositories['trigger'] = new Trigger(this.databaseMillix); // initialize shard 0 (root) const dbShard = new Shard(); diff --git a/database/repositories/repositories.js b/database/repositories/repositories.js index 2bef62a5..770c6484 100644 --- a/database/repositories/repositories.js +++ b/database/repositories/repositories.js @@ -10,9 +10,10 @@ import Schema from './schema'; import Job from './job'; import API from './api'; import Shard from './shard'; +import Trigger from './trigger'; export { Node, Keychain, Config, AuditPoint, Wallet, Address, AuditVerification, Transaction, Schema, Job, API, - Shard + Shard, Trigger }; diff --git a/database/repositories/trigger.js b/database/repositories/trigger.js new file mode 100644 index 00000000..a29e0142 --- /dev/null +++ b/database/repositories/trigger.js @@ -0,0 +1,366 @@ +import {Database} from '../database'; +import console from '../../core/console'; +import _ from 'lodash'; +import ntp from '../../core/ntp'; + + +export default class Trigger { + constructor(database) { + this.database = database; + } + + addTrigger(trigger) { + return new Promise((resolve, reject) => { + this.database.run('INSERT INTO trigger (trigger_name, trigger_type, object_guid, object_key, shard_id, data_source, data_source_type, data_source_variable, variable_1, variable_2, variable_operator, allow_adhoc) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ + trigger.name, + trigger.type, + trigger.object_guid, + trigger.object_key, + trigger.shard_id, + trigger.data_source, + trigger.data_source_type, + trigger.data_source_variable, + trigger.variable_1, + trigger.variable_2, + trigger.variable_operator, + trigger.allow_adhoc + ], (err) => { + if (err) { + return reject(err.message); + } + else { + console.log(`[database] Inserted trigger ${trigger.name} of type ${trigger.type}`); + resolve(); + } + }); + }); + } + + addTriggerWithActions(trigger, actions) { + const self = this; + return new Promise((resolve, reject) => { + this.database.run('BEGIN TRANSACTION', err => { + if (err) { + return reject(err.message); + } + + this.database.run('INSERT INTO trigger (trigger_name, trigger_type, object_guid, object_key, shard_id, data_source, data_source_type, data_source_variable, variable_1, variable_2, variable_operator, allow_adhoc) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); SELECT last_insert_rowid', [ + trigger.name, + trigger.type, + trigger.object_guid, + trigger.object_key, + trigger.shard_id, + trigger.data_source, + trigger.data_source_type, + JSON.stringify(trigger.data_source_variable), + trigger.variable_1, + trigger.variable_2, + trigger.variable_operator, + trigger.allow_adhoc + ], function(err) { + if (err) { + return reject(err.message); + } + + console.log(`[database] Inserted trigger ${trigger.name} of type ${trigger.type}`); + + for (let action of actions) { + self.addAction(this.lastID, action) + .catch(err => { + return reject(err.message); + }); + } + + console.log(`[database] Inserted ${actions.length} trigger actions`); + + self.database.run('COMMIT', (err) => { + if (err) { + return reject(err.message); + } + else { + console.log(`[database] Successfully commited database transaction`); + resolve(this.lastID); + } + }); + }); + }); + }); + } + + getTrigger(where) { + return new Promise((resolve, reject) => { + let {sql, parameters} = Database.buildQuery('SELECT * FROM trigger', where); + this.database.get(sql, parameters, (err, row) => { + if (err) { + return reject(err.message); + } + else { + row.data_source_variable = JSON.parse(row.data_source_variable); + resolve(row); + } + }); + }); + } + + getTriggerId(triggerName) { + return new Promise((resolve, reject) => { + this.database.get('SELECT id FROM trigger WHERE trigger_name = ?', [triggerName], (err, row) => { + if (err) { + return reject(err.message); + } + else { + if (row === undefined) { + resolve(null); + } + else { + resolve(row.id); + } + } + }); + }); + } + + getAllTriggers() { + return new Promise((resolve, reject) => { + this.database.all('SELECT * FROM trigger', [], (err, rows) => { + if (err) { + reject(err.message); + } + else { + for (let row of rows) { + row.data_source_variable = JSON.parse(row.data_source_variable); + } + resolve(rows); + } + }); + }); + } + + updateTrigger(triggerID, trigger) { + return new Promise((resolve, reject) => { + let set = _.pick(trigger, [ + 'name', + 'type', + 'object_guid', + 'object_key', + 'shard_id', + 'data_source', + 'data_source_type', + 'data_source_variable', + 'variable_1', + 'variable_2', + 'variable_operator', + 'allow_adhoc', + 'status' + ]); + set['trigger_name'] = set['name']; + delete set['name']; + set['trigger_type'] = set['type']; + delete set['type']; + set['update_date'] = Math.floor(ntp.now().getTime() / 1000); + const {sql, parameters} = Database.buildUpdate('UPDATE trigger', set, {id: triggerID}); + this.database.run(sql, parameters, (err) => { + if (err) { + reject(err.message); + } + else { + console.log(`[database] Updated trigger with id ${triggerID}`); + resolve(); + } + }); + }); + } + + updateTriggerAction(triggerID, triggerAction) { + return new Promise((resolve, reject) => { + let set = _.pick(triggerAction, [ + 'name', + 'trigger_result', + 'action', + 'action_variable', + 'priority', + 'status' + ]); + if (set.action !== undefined) { + set.action = JSON.stringify(set.action); + } + set['update_date'] = Math.floor(ntp.now().getTime() / 1000); + const {sql, parameters} = Database.buildUpdate('UPDATE trigger_action', set, { + trigger_id: triggerID, + name : triggerAction.name + }); + this.database.run(sql, parameters, (err) => { + if (err) { + reject(err.message); + } + else { + console.log(`[database] Updated trigger action ${triggerAction.name}`); + resolve(); + } + }); + }); + } + + setLastTriggerStatus(triggerID, status) { + return new Promise((resolve, reject) => { + this.database.run(`UPDATE trigger SET last_trigger_state = ?, update_date = ? WHERE id = ?`, [ + status, + Math.floor(ntp.now().getTime() / 1000), + triggerID + ], (err) => { + if (err) { + reject(err.message); + } + else { + console.log(`[database] Set trigger ${triggerID} last status to ${status}`); + resolve(); + } + }); + }); + } + + setTriggerActionResult(triggerActionID, result, timestamp) { + return new Promise((resolve, reject) => { + this.database.run('UPDATE trigger_action SET last_action_message = ?, last_action_date = ?, update_date = ? WHERE id = ?', [ + result, + timestamp, + timestamp, + triggerActionID + ], (err) => { + if (err) { + reject(err.message); + } else { + console.log('[database] Updated trigger action result'); + resolve(); + } + }); + }); + } + + checkTriggerExists(triggerName) { + return new Promise((resolve, reject) => { + this.database.get('SELECT id FROM trigger WHERE trigger_name = ?', [triggerName], (err, row) => { + if (err) { + reject(err.message); + } + else { + resolve(row !== undefined); + } + }); + }); + } + + checkTriggerActionExists(triggerName, action) { + return new Promise((resolve, reject) => { + this.database.get('SELECT trigger_action.id FROM trigger_action INNER JOIN trigger ON trigger_action.trigger_id = trigger.id WHERE trigger.name = ? AND trigger_action.action = ?', [ + triggerName, + action + ], (err, row) => { + if (err) { + reject(err.message); + } + else { + resolve(row !== undefined); + } + }); + }); + } + + removeTrigger(triggerName, triggerID) { + return new Promise((resolve, reject) => { + this.database.run('BEGIN TRANSACTION', err => { + if (err) { + return reject(err.message); + } + + this.database.run('DELETE FROM trigger WHERE trigger_name = ?', [triggerName], (err) => { + if (err) { + return reject(err.message); + } + else { + this.database.run('DELETE FROM trigger_action WHERE trigger_id = ?', [triggerID], (err) => { + if (err) { + return reject(err.message); + } + else { + this.database.run('COMMIT', (err) => { + if (err) { + return reject(err.message); + } + else { + console.log(`[database] Removed trigger ${triggerName}`); + resolve(); + } + }); + } + }); + } + }); + }); + }); + } + + removeTriggerAction(triggerID, actionName) { + return new Promise((resolve, reject) => { + this.database.run('DELETE FROM trigger_action WHERE trigger_id = ? AND name = ?', [ + triggerID, + actionName + ], (err) => { + if (err) { + return reject(err.message); + } + else { + console.log(`[database] Removed trigger action`); + resolve(); + } + }); + }); + } + + addAction(triggerID, triggerAction) { + return new Promise((resolve, reject) => { + this.database.run('INSERT INTO trigger_action (trigger_id, name, trigger_result, action, action_variable, priority) VALUES (?, ?, ?, ?, ?, ?)', [ + triggerID, + triggerAction.name, + triggerAction.trigger_result, + JSON.stringify(triggerAction.action), + triggerAction.action_variable, + triggerAction.priority + ], (err) => { + if (err) { + return reject(err.message); + } + else { + console.log(`[database] Inserted trigger action ${triggerAction.name} for trigger ID ${triggerID}`); + resolve(); + } + }); + }); + } + + getActions(trigger_id) { + return new Promise((resolve, reject) => { + this.database.all('SELECT * FROM trigger_action WHERE trigger_id = ?', [trigger_id], (err, rows) => { + if (err) { + return reject(err.message); + } + else { + resolve(rows); + } + }); + }); + } + + getAllActions() { + return new Promise((resolve, reject) => { + this.database.all('SELECT * FROM trigger_action', [], (err, rows) => { + if (err) { + return reject(err.message); + } + else { + resolve(rows); + } + }); + }); + } +} diff --git a/package.json b/package.json index 32a80986..194e0d1d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@babel/register": "7.8.3", "async": "^2.6.2", + "axios": "^0.20.0", "bitcore-lib": "^8.1.1", "bitcore-mnemonic": "^8.1.1", "body-parser": "^1.19.0", diff --git a/scripts/migration/schema-update-9.sql b/scripts/migration/schema-update-9.sql new file mode 100644 index 00000000..98e8ccd2 --- /dev/null +++ b/scripts/migration/schema-update-9.sql @@ -0,0 +1,56 @@ +PRAGMA foreign_keys= off; + +BEGIN TRANSACTION; + +UPDATE schema_information SET value = "9" WHERE key = "version"; + + +CREATE TABLE trigger +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trigger_name VARCHAR(200) NOT NULL UNIQUE, + trigger_type CHAR(32) NOT NULL, + object_guid CHAR(32), + object_key VARCHAR(200), + shard_id CHAR(32), + data_source VARCHAR(1000), + data_source_type VARCHAR(1000), + data_source_variable VARCHAR(4000), + variable_1 VARCHAR(200), + variable_2 VARCHAR(200), + variable_operator VARCHAR(45), + last_trigger_state TINYINT NOT NULL DEFAULT 0, + allow_adhoc TINYINT NOT NULL DEFAULT 0, + status TINYINT NOT NULL DEFAULT 1, + create_date INT NOT NULL DEFAULT (strftime('%s', 'now')), + update_date INT NOT NULL DEFAULT (strftime('%s', 'now')) +); + +CREATE TABLE trigger_action +( + id INTEGER PRIMARY KEY, + trigger_id INTEGER NOT NULL, + name CHAR(200) NOT NULL UNIQUE, + trigger_result VARCHAR(1000) NOT NULL, + action VARCHAR(1000) NOT NULL, + action_variable VARCHAR(4000), + last_action_message VARCHAR(1000), + last_action_date INT, + priority SMALLINT(6) NOT NULL DEFAULT 0, + status SMALLINT NOT NULL DEFAULT 1, + create_date INT NOT NULL DEFAULT (strftime('%s', 'now')), + update_date INT NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (trigger_id) references trigger(id) ON DELETE CASCADE +); + + +INSERT INTO api(api_id, name, description, method, version_released, permission) VALUES + ("8s9Auqlc7RHbpxsJ", "invoke_trigger", "invokes trigger, kicking off trigger actions if requirements satisfied", "POST", "1.4.0", '{"require_identity": true, "private": true}'), + ("GxFK1FzbVyWUx8MR", "add_trigger_action", "adds trigger action for existing trigger", "POST", "1.4.0", '{"require_identity": true, "private": true}'), + ("Hehn7N0KBVvuFQHf", "add_trigger", "creates new trigger with optional trigger actions", "POST", "1.4.0", '{"require_identity": true, "private": true}'), + ("RlUQHxrBmf1ryge3", "remove_trigger_action", "removes a trigger action for an existing trigger", "POST", "1.4.0", '{"require_identity": true, "private": true}'), + ("tLog09WkateEmT81", "update_trigger_action", "updates an existing trigger action", "POST", "1.4.0", '{"require_identity": true, "private": true}'), + ("vu35sd0ipsex2pDf", "remove_trigger", "removes an existing trigger with its actions", "POST", "1.4.0", '{"require_identity": true, "private": true}'), + ("xnm05bThUrJjxkqT", "update_trigger", "updates an existing trigger", "POST", "1.4.0", '{"require_identity": true, "private": true}'); + +COMMIT; diff --git a/scripts/migration/shard/schema-update-9.sql b/scripts/migration/shard/schema-update-9.sql new file mode 100644 index 00000000..fd520126 --- /dev/null +++ b/scripts/migration/shard/schema-update-9.sql @@ -0,0 +1,7 @@ +PRAGMA foreign_keys= off; + +BEGIN TRANSACTION; + +UPDATE schema_information SET value = "9" WHERE key = "version"; + +COMMIT; diff --git a/trigger/trigger.js b/trigger/trigger.js new file mode 100644 index 00000000..5cb24ac2 --- /dev/null +++ b/trigger/trigger.js @@ -0,0 +1,367 @@ +import database from '../database/database'; +import ntp from '../core/ntp'; +import axios from 'axios'; + + +function test() { + console.log('\n\n\nTEEEST\n\n'); +} + + +class TriggerEngine { + constructor() { + this._triggers = {}; + this._triggerNamesToIDs = {}; + this._triggerActions = {}; + this.initialized = false; + this.repository = undefined; + this._modules = { + 'test': { + test: test + } + }; // TODO + } + + // Reads all the triggers and trigger actions and stores it in memory + initialize() { + this.repository = database.getRepository('trigger'); + console.log('[Trigger Engine] Initializing trigger engine. Getting triggers from the database'); + + return this.repository.getAllTriggers() + .then(trigger_rows => { + for (let triggerRow of trigger_rows) { + console.log(`[Trigger Engine] Trigger Row: ${JSON.stringify(triggerRow)}`); + this._triggers[triggerRow.id] = triggerRow; + this._triggerNamesToIDs[triggerRow.trigger_name] = triggerRow.id; + this._triggerActions[triggerRow.id] = []; + } + + console.log(`[Trigger Engine] ${trigger_rows.length} triggers retrieved from the database`); + console.log('[Trigger Engine] Getting trigger actions from the database'); + + this.repository.getAllActions() + .then(triggerActionRows => { + for (let triggerActionRow of triggerActionRows) { + triggerActionRow.action = JSON.parse(triggerActionRow.action); + const triggerID = triggerActionRow.trigger_id; + this._triggerActions[triggerID].push(triggerActionRow); + } + + console.log(`[Trigger Engine] ${triggerActionRows.length} trigger actions retrieved from the database`); + console.log(`[Trigger Engine] Successfully initialized`); + + this.initialized = true; + }) + .catch(err => { + console.log(`[Trigger Engine] Failed to get all actions: ${err}`); + throw err; + }); + }) + .catch(err => { + console.log(`[Trigger Engine] Error while initializing: ${err}`); + throw Error('Could not initialize trigger engine'); + }); + } + + addTrigger(trigger, triggerActions) { + console.log('[Trigger Engine] Adding new trigger'); + + return this.repository.addTriggerWithActions(trigger, triggerActions) + .then(triggerID => { + this._triggerNamesToIDs[trigger.name] = triggerID; + this._triggers[triggerID] = trigger; + this._triggerActions[triggerID] = triggerActions; + + console.log('[Trigger Engine] Successfully added new trigger'); + }) + .catch(err => { + throw Error(`Could not store trigger. Error: ${err}`); + }); + } + + addTriggerAction(triggerName, action) { + console.log(`[Trigger Engine] Adding trigger action to trigger ${triggerName}. Retrieving trigger ID`); + + const triggerID = this._triggerNamesToIDs[triggerName]; + + return this.repository.addAction(triggerID, action) + .then(_ => { + this._triggerActions[triggerID].push(action); + console.log('[Trigger Engine] Successfully added new trigger action'); + }) + .catch(err => { + throw Error(`Could not add trigger action. Error: ${err}`); + }); + } + + checkTriggerExists(triggerName) { + console.log(`[Trigger Engine] Checking if trigger ${triggerName} exists`); + + return triggerName in this._triggerNamesToIDs; + } + + isTriggerDisabled(triggerName) { + const triggerID = this._triggerNamesToIDs[triggerName]; + const trigger = this._triggers[triggerID]; + return (trigger.status === 0); + } + + checkActionExists(triggerName, actionName) { + console.log(`[Trigger Engine] Checking if trigger action ${actionName} exists for trigger ${triggerName}`); + + const triggerID = this._triggerNamesToIDs[triggerName]; + + if (!(triggerID)) { + throw Error('No such trigger'); + } + + const triggerActions = this._triggerActions[triggerID]; + + for (let triggerAction of triggerActions) { + if (triggerAction.name === actionName) { + return true; + } + } + + return false; + } + + // Trigger contains all of the fields that we want to update + updateTrigger(triggerName, trigger) { + const triggerID = this._triggerNamesToIDs[triggerName]; + + return this.repository.updateTrigger(triggerID, trigger) + .then(_ => { + let existingTrigger = this._triggers[triggerID]; + + for (let fieldName of Object.keys(trigger)) { + // Making sure it sets only fields that exist (user + // might send additional fields by error) + if (fieldName in existingTrigger) { + existingTrigger[fieldName] = trigger[fieldName]; + } + } + + console.log(`[Trigger Engine] Updated trigger ${triggerName}`); + }) + .catch(err => { + throw Error(`Could not update trigger ${triggerName}. Error: ${err}`); + }); + } + + updateTriggerAction(triggerName, action) { + const triggerID = this._triggerNamesToIDs[triggerName]; + + return this.repository.updateTriggerAction(triggerID, action) + .then(_ => { + let actions = this._triggerActions[triggerID]; + + for (let existingAction of actions) { + if (existingAction.name === action.name) { + for (let fieldName of Object.keys(action)) { + // Making sure it sets only fields that + // exist (user might send additional fields + // by error) + if (fieldName in existingAction) { + existingAction[fieldName] = action[fieldName]; + } + } + } + } + + console.log(`[Trigger Engine] Updated trigger action ${action.name}`); + }) + .catch(err => { + throw Error(`Could not update trigger action ${action.name}. Error: ${err}`); + }); + } + + removeTrigger(triggerName) { + console.log(`[Trigger Engine] Removing trigger ${triggerName}`); + const triggerID = this._triggerNamesToIDs[triggerName]; + + return this.repository.removeTrigger(triggerName, triggerID) + .then(_ => { + delete this._triggerNamesToIDs[triggerName]; + delete this._triggers[triggerID]; + delete this._triggerActions[triggerID]; + + console.log(`[Trigger Engine] Removed trigger ${triggerName}`); + }) + .catch(err => { + throw Error(`Could not remove trigger ${triggerName}. Error: ${err}`); + }); + } + + removeTriggerAction(triggerName, action) { + console.log(`[Trigger Engine] Removing trigger action for trigger ${triggerName}`); + const triggerID = this._triggerNamesToIDs[triggerName]; + + return this.repository.removeTriggerAction(triggerID, action) + .then(_ => { + let actions = this._triggerActions[triggerID]; + actions = actions.filter(a => a.action !== action); + this._triggerActions[triggerID] = actions; + + console.log(`[Trigger Engine] Removed trigger action`); + }) + .catch(err => { + throw Error(`Could not remove trigger. Error: ${err}`); + }); + } + + invokeTrigger(triggerName) { + const triggerID = this._triggerNamesToIDs[triggerName]; + let status; + + return this._invokeTrigger(triggerName) + .then(_ => { + console.log(`[Trigger Engine] Invoked trigger ${triggerName}. Setting last state to 1`); + status = 1; + this.repository.setLastTriggerStatus(triggerID, status); + }) + .catch(err => { + console.log(`[Trigger Engine] Error while invoking trigger ${triggerName}. Error: ${err}`); + status = 0; + this.repository.setLastTriggerStatus(triggerID, status); + throw err; + }); + } + + // Called by the API endpoint + _invokeTrigger(triggerName) { + const triggerID = this._triggerNamesToIDs[triggerName]; + const trigger = this._triggers[triggerID]; + + if (trigger.data_source_type !== 'url') { + throw new Error(`Unsupported data source type ${trigger.data_source_type}`); + } + + return this._invokeURLTrigger(trigger).then(requestResult => { + const actualValue = requestResult[trigger.variable_1]; + if (actualValue === undefined) { + throw new Error(`No field ${trigger.variable_1} in the response`); + } + + const thresholdValue = trigger.variable_2; + const operator = trigger.variable_operator; + + let res; + switch (operator) { + case '=': + if (actualValue === thresholdValue) { + res = this._activateTrigger(triggerName); + } + break; + case '<': + if (actualValue < thresholdValue) { + res = this._activateTrigger(triggerName); + } + break; + case '<=': + if (actualValue <= thresholdValue) { + res = this._activateTrigger(triggerName); + } + break; + case '>': + if (actualValue > thresholdValue) { + res = this._activateTrigger(triggerName); + } + break; + case '>=': + if (actualValue >= thresholdValue) { + res = this._activateTrigger(triggerName); + } + break; + default: + throw new Error(`Invalid operator ${operator}`); + } + + return res; + }); + } + + _invokeURLTrigger(trigger) { + console.log('[Trigger Engine] Invoking trigger with an URL data source'); + let url = trigger.data_source; + const vars = trigger.data_source_variable; + + for (let varName of Object.keys(vars)) { + const varValue = vars[varName]; + url = url.replace(`[${varName}]`, varValue); + } + + console.log(`[Trigger Engine] Full URL: ${url}`); + + return axios.get(url) + .then(response => { + return response.data; + }) + .catch(err => { + throw new Error(`Failed to perform HTTP request: ${err.message}`); + }); + } + + _activateTrigger(triggerName) { + const triggerID = this._triggerNamesToIDs[triggerName]; + const triggerActions = this._triggerActions[triggerID]; + + if (triggerActions.length === 0) { + console.log(`[Trigger Engine] No trigger actions for trigger ${triggerName}`); + return; + } + + console.log(`[Trigger Engine] Kicking of ${triggerActions.length} trigger actions for trigger ${triggerName}`); + + const timestamp = Math.floor(ntp.now().getTime() / 1000); + + let promises = []; + for (let triggerAction of triggerActions) { + if (triggerAction.status === 0) { + console.log(`[Trigger Engine] Skipping trigger action because it is disabled`); + continue; + } + + let message; + + try { + this._runTriggerAction(triggerAction); + message = "OK"; + } catch (err) { + // Making sure it is less 1000 characters + message = err.message.substring(0, 1000); + } + + promises.push(this.repository.setTriggerActionResult(triggerAction.id, message, timestamp)); + } + + let promise = Promise.all(promises); + return promise; + } + + _runTriggerAction(triggerAction) { + const action = triggerAction.action; + + console.log(`[Trigger Action] Executing trigger action ${JSON.stringify(action)}`); + + for (let step of action) { + if (step.action_type === 'function') { + try { + this._invokeActionFunction(step.module, step.function); + } + catch (err) { + throw Error(`Error while invoking trigger action: ${err}`); + } + } + } + } + + _invokeActionFunction(moduleName, functionName) { + console.log(`[Trigger Engine] Invoking ${moduleName}.${functionName}`); + const module = this._modules[moduleName]; + module[functionName](); + } +} + + +export default new TriggerEngine(); diff --git a/trigger/validation.js b/trigger/validation.js new file mode 100644 index 00000000..7983d9a1 --- /dev/null +++ b/trigger/validation.js @@ -0,0 +1,75 @@ +const nameRegex = new RegExp("^[0-9A-Za-z_.-]+$"); +const triggerTypeRegex = new RegExp("^[0-9A-Za-z]+$"); + + + +function validateTrigger(trigger) { + _validateString(trigger.name,200, true, "name", nameRegex); + _validateString(trigger.type, 32, true, "type", triggerTypeRegex); + _validateString(trigger.object_guid, 32, false, "object_guid"); + _validateString(trigger.object_key, 200, false, "object_key"); + _validateString(trigger.shard_id, 32, false, "shard_id"); + _validateString(trigger.data_source, 1000, true, "data_source"); + _validateString(trigger.data_source_type, 1000, true, "data_source_type"); + _validateObject(trigger.data_source_variable, "data_source_variable"); + _validateString(trigger.variable_1, 200, true, "variable_1"); + _validateString(trigger.variable_2, 200, true, "variable_2"); + _validateString(trigger.variable_operator, 40, true, "variable_operator"); + _validateAllowAdhoc(trigger.allow_adhoc); +} + + +function _validateAllowAdhoc(allowAdhoc) { + if (allowAdhoc === undefined || allowAdhoc === null) { + return; + } + + if (!(typeof allowAdhoc === 'boolean' || allowAdhoc instanceof Boolean)) { + throw Error("Allow adhoc must be boolean"); + } +} + +function _validateObject(o, requiredFields, fieldName) { + if (!(o instanceof Object)) { + throw Error(`${fieldName} must be object`); + } +} + + +function validateTriggerAction(triggerAction) { + _validateString(triggerAction.name, 200, true, "name"); + _validateTriggerActionPriority(triggerAction.priority); +} + +function _validateString(s, length, necessary, fieldName, regex) { + if (necessary && (!(s))) { + throw Error(`${fieldName} is necessary but not set`) + } + + if (!(necessary) && (!(s))) { + return; + } + + if (!(typeof s === 'string' || s instanceof String)) { + throw Error(`${fieldName} must be string`); + } + + if (s.length > length) { + throw Error(`${fieldName} cannot be longer than 32 characters`) + } + + if (regex !== undefined) { + if (!(regex.test(s))) { + throw Error(`${fieldName} contains invalid characters`); + } + } +} + +function _validateTriggerActionPriority(priority) { + if (!(typeof priority === 'number' || priority instanceof Number)) { + throw Error('priority must be integer') + } +} + +exports.validateTrigger = validateTrigger; +exports.validateTriggerAction = validateTriggerAction;