revamp db - refactor webhooks

This commit is contained in:
Dave Horton
2020-01-28 10:30:07 -05:00
parent 10074504c5
commit 87208bc669
13 changed files with 675 additions and 252 deletions
+55 -14
View File
@@ -1,8 +1,35 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const listSqlSp = 'SELECT * from accounts WHERE service_provider_sid = ?';
const listSqlAccount = 'SELECT * from accounts WHERE account_sid = ?';
const retrieveSql = 'SELECT * from accounts WHERE service_provider_sid = ? AND account_sid = ?';
const retrieveSql = `SELECT * from accounts acc
LEFT JOIN webhooks AS rh
ON acc.registration_hook_sid = rh.webhook_sid
LEFT JOIN webhooks AS dh
ON acc.device_calling_hook_sid = dh.webhook_sid
LEFT JOIN webhooks AS eh
ON acc.error_hook_sid = eh.webhook_sid`;
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.acc;
if (row.rh && Object.keys(row.rh).length && row.rh.url !== null) {
Object.assign(obj, {registration_hook: row.rh});
}
else obj.registration_hook = null;
if (row.dh && Object.keys(row.dh).length && row.dh.url !== null) {
Object.assign(obj, {device_calling_hook: row.dh});
}
else obj.device_calling_hook = null;
if (row.eh && Object.keys(row.eh).length && row.eh.url !== null) {
Object.assign(obj, {error_hook: row.eh});
}
else obj.error_hook = null;
delete obj.registration_hook_sid;
delete obj.device_calling_hook_sid;
delete obj.error_hook_sid;
return obj;
});
}
class Account extends Model {
constructor() {
@@ -13,16 +40,24 @@ class Account extends Model {
* list all accounts
*/
static retrieveAll(service_provider_sid, account_sid) {
if (!service_provider_sid && !account_sid) return super.retrieveAll();
let sql = retrieveSql;
const args = [];
if (account_sid) {
sql = `${sql} WHERE acc.account_sid = ?`;
args.push(account_sid);
}
else if (service_provider_sid) {
sql = `${sql} WHERE acc.service_provider_sid = ?`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
const sql = account_sid ? listSqlAccount : listSqlSp;
const args = account_sid ? [account_sid] : [service_provider_sid];
conn.query(sql, args, (err, results, fields) => {
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
@@ -32,14 +67,20 @@ class Account extends Model {
* retrieve an account
*/
static retrieve(sid, service_provider_sid) {
if (!service_provider_sid) return super.retrieve(sid);
const args = [sid];
let sql = `${retrieveSql} WHERE acc.account_sid = ?`;
if (service_provider_sid) {
sql = `${retrieveSql} WHERE acc.account_sid = ? AND acc.service_provider_sid = ?`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(retrieveSql, [service_provider_sid, sid], (err, results, fields) => {
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
@@ -69,15 +110,15 @@ Account.fields = [
type: 'string',
},
{
name: 'registration_hook',
name: 'registration_hook_sid',
type: 'string',
},
{
name: 'hook_basic_auth_user',
name: 'device_calling_hook_sid',
type: 'string',
},
{
name: 'hook_basic_auth_password',
name: 'error_hook_sid',
type: 'string',
}
];
+51 -24
View File
@@ -19,6 +19,29 @@ SELECT * from applications
WHERE account_sid = ?
AND application_sid = ?`;
const retrieveSql = `SELECT * from applications app
LEFT JOIN webhooks AS ch
ON app.call_hook_sid = ch.webhook_sid
LEFT JOIN webhooks AS sh
ON app.call_status_hook_sid = sh.webhook_sid`;
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.app;
if (row.ch && Object.keys(row.ch).length && row.ch.url !== null) {
Object.assign(obj, {call_hook: row.ch});
}
else obj.call_hook = null;
if (row.sh && Object.keys(row.sh).length && row.sh.url !== null) {
Object.assign(obj, {call_status_hook: row.sh});
}
else obj.call_status_hook = null;
delete obj.call_hook_sid;
delete obj.call_status_hook_sid;
return obj;
});
}
class Application extends Model {
constructor() {
super();
@@ -28,16 +51,24 @@ class Application extends Model {
* list all applications - for all service providers, for one service provider, or for one account
*/
static retrieveAll(service_provider_sid, account_sid) {
if (!service_provider_sid && !account_sid) return super.retrieveAll();
let sql = retrieveSql;
const args = [];
if (account_sid) {
sql = `${sql} WHERE app.account_sid = ?`;
args.push(account_sid);
}
else if (service_provider_sid) {
sql = `${sql} WHERE account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
const sql = account_sid ? listSqlAccount : listSqlSp;
const args = account_sid ? [account_sid] : [service_provider_sid];
conn.query(sql, args, (err, results, fields) => {
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
@@ -47,16 +78,24 @@ class Application extends Model {
* retrieve an application
*/
static retrieve(sid, service_provider_sid, account_sid) {
if (!service_provider_sid && !account_sid) return super.retrieve(sid);
const args = [sid];
let sql = `${retrieveSql} WHERE app.application_sid = ?`;
if (account_sid) {
sql = `${sql} AND app.account_sid = ?`;
args.push(account_sid);
}
if (service_provider_sid) {
sql = `${sql} AND account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
const sql = account_sid ? retrieveSqlAccount : retrieveSqlSp;
const args = account_sid ? [account_sid, sid] : [service_provider_sid, sid];
conn.query(sql, args, (err, results, fields) => {
conn.query({sql, nestTables: true}, args, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
@@ -82,26 +121,14 @@ Application.fields = [
required: true
},
{
name: 'call_hook',
name: 'call_hook_sid',
type: 'string',
required: true
},
{
name: 'call_status_hook',
name: 'call_status_hook_sid',
type: 'string',
required: true
},
{
name: 'hook_basic_auth_user',
type: 'string',
},
{
name: 'hook_basic_auth_password',
type: 'string',
},
{
name: 'hook_http_method',
type: 'string',
}
];
+35
View File
@@ -0,0 +1,35 @@
const Model = require('./model');
class Webhook extends Model {
constructor() {
super();
}
}
Webhook.table = 'webhooks';
Webhook.fields = [
{
name: 'webhook_sid',
type: 'string',
primaryKey: true
},
{
name: 'url',
type: 'string',
required: true
},
{
name: 'method',
type: 'string'
},
{
name: 'username',
type: 'string'
},
{
name: 'password',
type: 'string'
}
];
module.exports = Webhook;
+71 -1
View File
@@ -1,6 +1,7 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const ServiceProvider = require('../../models/service-provider');
const decorate = require('./decorate');
const sysError = require('./error');
@@ -25,6 +26,16 @@ async function validateAdd(req) {
throw new DbErrorBadRequest(`service_provider not found for sid ${req.body.service_provider_sid}`);
}
}
if (req.body.registration_hook && typeof req.body.registration_hook !== 'object') {
throw new DbErrorBadRequest('\'registration_hook\' must be an object when adding an account');
}
if (req.body.device_calling_hook && typeof req.body.device_calling_hook !== 'object') {
throw new DbErrorBadRequest('\'device_calling_hook\' must be an object when adding an account');
}
if (req.body.error_hook && typeof req.body.error_hook !== 'object') {
throw new DbErrorBadRequest('\'error_hook\' must be an object when adding an account');
}
}
async function validateUpdate(req, sid) {
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
@@ -53,7 +64,30 @@ async function validateDelete(req, sid) {
}
}
decorate(router, Account, ['add', 'update', 'delete'], preconditions);
decorate(router, Account, ['delete'], preconditions);
/* add */
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateAdd(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook', 'device_calling_hook', 'error_hook']) {
if (obj[prop]) {
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
delete obj[prop];
}
}
//logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
const uuid = await Account.make(obj);
res.status(201).json({sid: uuid});
} catch (err) {
sysError(logger, res, err);
}
});
/* list */
router.get('/', async(req, res) => {
@@ -82,4 +116,40 @@ router.get('/:sid', async(req, res) => {
}
});
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['registration_hook', 'device_calling_hook', 'error_hook']) {
if (prop in obj && Object.keys(obj[prop]).length) {
if ('webhook_sid' in obj[prop]) {
const sid = obj[prop]['webhook_sid'];
delete obj[prop]['webhook_sid'];
await Webhook.update(sid, obj[prop]);
}
else {
const sid = await Webhook.make(obj[prop]);
obj[`${prop}_sid`] = sid;
}
}
else {
obj[`${prop}_sid`] = null;
}
delete obj[prop];
}
await validateUpdate(req, sid);
const rowsAffected = await Account.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;
+73 -1
View File
@@ -2,6 +2,7 @@ const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Application = require('../../models/application');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const decorate = require('./decorate');
const sysError = require('./error');
const preconditions = {
@@ -23,12 +24,24 @@ async function validateAdd(req) {
throw new DbErrorBadRequest('insufficient privileges to create an application under the specified account');
}
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_hook\' must be an object when adding an application');
}
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when adding an application');
}
}
async function validateUpdate(req, sid) {
if (req.user.account_sid && sid !== req.user.account_sid) {
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
}
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_hook\' must be an object when updating an application');
}
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when updating an application');
}
}
async function validateDelete(req, sid) {
@@ -39,7 +52,29 @@ async function validateDelete(req, sid) {
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
}
decorate(router, Application, ['add', 'update', 'delete'], preconditions);
decorate(router, Application, ['delete'], preconditions);
/* add */
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateAdd(req);
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['call_hook', 'call_status_hook']) {
if (obj[prop]) {
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
delete obj[prop];
}
}
const uuid = await Application.make(obj);
res.status(201).json({sid: uuid});
} catch (err) {
sysError(logger, res, err);
}
});
/* list */
router.get('/', async(req, res) => {
@@ -69,4 +104,41 @@ router.get('/:sid', async(req, res) => {
}
});
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
await validateUpdate(req, sid);
// create webhooks if provided
const obj = Object.assign({}, req.body);
for (const prop of ['call_hook', 'call_status_hook']) {
if (prop in obj && Object.keys(obj[prop]).length) {
if ('webhook_sid' in obj[prop]) {
const sid = obj[prop]['webhook_sid'];
delete obj[prop]['webhook_sid'];
await Webhook.update(sid, obj[prop]);
}
else {
const sid = await Webhook.make(obj[prop]);
obj[`${prop}_sid`] = sid;
}
}
else {
obj[`${prop}_sid`] = null;
}
delete obj[prop];
}
const rowsAffected = await Application.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;
+50 -49
View File
@@ -655,20 +655,18 @@ paths:
description: sip realm for registration
example: sip.mycompany.com
registration_hook:
type: string
format: url
$ref: '#/components/Webhook'
description: authentication webhook for registration
example: https://mycompany.com
device_calling_hook:
$ref: '#/components/Webhook'
description: webhook for inbound call from registered devices
error_hook:
$ref: '#/components/Webhook'
description: webhook for reporting errors from malformed applications
service_provider_sid:
type: string
format: uuid
example: 85f9c036-ba61-4f28-b2f5-617c23fa68ff
hook_basic_auth_user:
type: string
description: username to use for http basic auth when calling hook
hook_basic_auth_password:
type: string
description: password to use for http basic auth when calling hook
required:
- name
- service_provider_sid
@@ -797,10 +795,6 @@ paths:
type: string
format: url
description: url of application to invoke when this device places a call
call_status_hook:
type: string
format: url
description: url of application to invoke for call status events on calls placed from this device
expires:
type: number
description: |
@@ -922,25 +916,19 @@ paths:
type: string
format: uuid
call_hook:
type: string
format: url
description: webhook to invoke when call is received
$ref: '#/components/Webhook'
description: authentication webhook for inbound calls from PSTN
call_status_hook:
$ref: '#/components/Webhook'
description: webhook for call status events
speech_synthesis_vendor:
type: string
format: url
description: webhook to pass call status updates to
hook_basic_auth_user:
speech_synthesis_voice:
type: string
description: username to use for http basic auth when calling hook
hook_basic_auth_password:
speech_recognizer_vendor:
type: string
description: password to use for http basic auth when calling hook
hook_http_method:
speech_recognizer_language:
type: string
description: whether to use GET or POST
enum:
- get
- post
required:
- name
- account_sid
@@ -1243,17 +1231,17 @@ components:
sip_realm:
type: string
registration_hook:
type: string
format: url
$ref: '#/components/Webhook'
description: authentication webhook for registration
device_calling_hook:
$ref: '#/components/Webhook'
description: webhook for inbound call from registered devices
error_hook:
$ref: '#/components/Webhook'
description: webhook for reporting errors from malformed applications
service_provider_sid:
type: string
format: uuid
hook_basic_auth_user:
type: string
format: url
hook_basic_auth_password:
type: string
format: url
required:
- account_sid
- name
@@ -1270,29 +1258,25 @@ components:
type: string
format: uuid
call_hook:
type: string
format: url
$ref: '#/components/Webhook'
description: authentication webhook for registration
call_status_hook:
$ref: '#/components/Webhook'
description: authentication webhook for registration
speech_synthesis_vendor:
type: string
format: url
hook_http_method:
speech_synthesis_voice:
type: string
enum:
- get
- post
default: get
hook_basic_auth_user:
speech_recognizer_vendor:
type: string
format: url
hook_basic_auth_password:
speech_recognizer_language:
type: string
format: url
required:
- application_sid
- name
- account_sid
- call_hook
- call_status_hook
- inbound_hook
- inbound_status_hook
PhoneNumber:
type: object
properties:
@@ -1362,6 +1346,23 @@ components:
- call_sid
- application
- direction
Webhook:
type: object
properties:
url:
type: string
format: url
method:
type: string
enum:
- get
- post
username:
type: string
password:
type: string
required:
- url
security:
- bearerAuth: []