/** @flow weak */ import * as triggers from '../triggers'; // @flow-disable-next import * as Parse from 'parse/node'; // @flow-disable-next import request from '../request'; import { logger } from '../logger'; import http from 'http'; import https from 'https'; const DefaultHooksCollectionName = '_Hooks'; const HTTPAgents = { http: new http.Agent({ keepAlive: true }), https: new https.Agent({ keepAlive: true }), }; export class HooksController { _applicationId: string; _webhookKey: string; database: any; constructor(applicationId: string, databaseController, webhookKey) { this._applicationId = applicationId; this._webhookKey = webhookKey; this.database = databaseController; } load() { return this._getHooks().then((hooks) => { hooks = hooks || []; hooks.forEach((hook) => { this.addHookToTriggers(hook); }); }); } getFunction(functionName) { return this._getHooks({ functionName: functionName }).then( (results) => results[0] ); } getFunctions() { return this._getHooks({ functionName: { $exists: true } }); } getTrigger(className, triggerName) { return this._getHooks({ className: className, triggerName: triggerName, }).then((results) => results[0]); } getTriggers() { return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true }, }); } deleteFunction(functionName) { triggers.removeFunction(functionName, this._applicationId); return this._removeHooks({ functionName: functionName }); } deleteTrigger(className, triggerName) { triggers.removeTrigger(triggerName, className, this._applicationId); return this._removeHooks({ className: className, triggerName: triggerName, }); } _getHooks(query = {}) { return this.database .find(DefaultHooksCollectionName, query) .then((results) => { return results.map((result) => { delete result.objectId; return result; }); }); } _removeHooks(query) { return this.database.destroy(DefaultHooksCollectionName, query).then(() => { return Promise.resolve({}); }); } saveHook(hook) { var query; if (hook.functionName && hook.url) { query = { functionName: hook.functionName }; } else if (hook.triggerName && hook.className && hook.url) { query = { className: hook.className, triggerName: hook.triggerName }; } else { throw new Parse.Error(143, 'invalid hook declaration'); } return this.database .update(DefaultHooksCollectionName, query, hook, { upsert: true }) .then(() => { return Promise.resolve(hook); }); } addHookToTriggers(hook) { var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey); wrappedFunction.url = hook.url; if (hook.className) { triggers.addTrigger( hook.triggerName, hook.className, wrappedFunction, this._applicationId ); } else { triggers.addFunction( hook.functionName, wrappedFunction, null, this._applicationId ); } } addHook(hook) { this.addHookToTriggers(hook); return this.saveHook(hook); } createOrUpdateHook(aHook) { var hook; if (aHook && aHook.functionName && aHook.url) { hook = {}; hook.functionName = aHook.functionName; hook.url = aHook.url; } else if ( aHook && aHook.className && aHook.url && aHook.triggerName && triggers.Types[aHook.triggerName] ) { hook = {}; hook.className = aHook.className; hook.url = aHook.url; hook.triggerName = aHook.triggerName; } else { throw new Parse.Error(143, 'invalid hook declaration'); } return this.addHook(hook); } createHook(aHook) { if (aHook.functionName) { return this.getFunction(aHook.functionName).then((result) => { if (result) { throw new Parse.Error( 143, `function name: ${aHook.functionName} already exits` ); } else { return this.createOrUpdateHook(aHook); } }); } else if (aHook.className && aHook.triggerName) { return this.getTrigger(aHook.className, aHook.triggerName).then( (result) => { if (result) { throw new Parse.Error( 143, `class ${aHook.className} already has trigger ${aHook.triggerName}` ); } return this.createOrUpdateHook(aHook); } ); } throw new Parse.Error(143, 'invalid hook declaration'); } updateHook(aHook) { if (aHook.functionName) { return this.getFunction(aHook.functionName).then((result) => { if (result) { return this.createOrUpdateHook(aHook); } throw new Parse.Error( 143, `no function named: ${aHook.functionName} is defined` ); }); } else if (aHook.className && aHook.triggerName) { return this.getTrigger(aHook.className, aHook.triggerName).then( (result) => { if (result) { return this.createOrUpdateHook(aHook); } throw new Parse.Error(143, `class ${aHook.className} does not exist`); } ); } throw new Parse.Error(143, 'invalid hook declaration'); } } function wrapToHTTPRequest(hook, key) { return (req) => { const jsonBody = {}; for (var i in req) { jsonBody[i] = req[i]; } if (req.object) { jsonBody.object = req.object.toJSON(); jsonBody.object.className = req.object.className; } if (req.original) { jsonBody.original = req.original.toJSON(); jsonBody.original.className = req.original.className; } const jsonRequest: any = { url: hook.url, headers: { 'Content-Type': 'application/json', }, body: jsonBody, method: 'POST', }; const agent = hook.url.startsWith('https') ? HTTPAgents['https'] : HTTPAgents['http']; jsonRequest.agent = agent; if (key) { jsonRequest.headers['X-Parse-Webhook-Key'] = key; } else { logger.warn( 'Making outgoing webhook request without webhookKey being set!' ); } return request(jsonRequest).then((response) => { let err; let result; let body = response.data; if (body) { if (typeof body === 'string') { try { body = JSON.parse(body); } catch (e) { err = { error: 'Malformed response', code: -1, partialResponse: body.substring(0, 100), }; } } if (!err) { result = body.success; err = body.error; } } if (err) { throw err; } else if (hook.triggerName === 'beforeSave') { if (typeof result === 'object') { delete result.createdAt; delete result.updatedAt; } return { object: result }; } else { return result; } }); }; } export default HooksController;