Merge pull request #787 from Budibase/feature/webhooks

Webhooks for automations
This commit is contained in:
Michael Drury 2020-10-27 14:24:30 +00:00 committed by GitHub
commit a3717ce041
31 changed files with 817 additions and 30 deletions

View File

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.44.1", "@budibase/bbui": "^1.47.0",
"@budibase/client": "^0.2.6", "@budibase/client": "^0.2.6",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",

View File

@ -11,12 +11,20 @@ const automationActions = store => ({
]) ])
const jsonResponses = await Promise.all(responses.map(x => x.json())) const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => { store.update(state => {
let selected = state.selectedAutomation?.automation
state.automations = jsonResponses[0] state.automations = jsonResponses[0]
state.blockDefinitions = { state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger, TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action, ACTION: jsonResponses[1].action,
LOGIC: jsonResponses[1].logic, LOGIC: jsonResponses[1].logic,
} }
// if previously selected find the new obj and select it
if (selected) {
selected = jsonResponses[0].filter(
automation => automation._id === selected._id
)
state.selectedAutomation = new Automation(selected[0])
}
return state return state
}) })
}, },

View File

@ -1,11 +1,18 @@
<script> <script>
import { automationStore } from "builderStore" import { backendUiStore, automationStore } from "builderStore"
import CreateWebookModal from "../../Shared/CreateWebhookModal.svelte"
import analytics from "analytics" import analytics from "analytics"
import { Modal } from "@budibase/bbui"
export let blockDefinition export let blockDefinition
export let stepId export let stepId
export let blockType export let blockType
let modal
$: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation
function addBlockToAutomation() { function addBlockToAutomation() {
automationStore.actions.addBlockToAutomation({ automationStore.actions.addBlockToAutomation({
...blockDefinition, ...blockDefinition,
@ -13,6 +20,9 @@
stepId, stepId,
type: blockType, type: blockType,
}) })
if (stepId === "WEBHOOK") {
modal.show()
}
analytics.captureEvent("Added Automation Block", { analytics.captureEvent("Added Automation Block", {
name: blockDefinition.name, name: blockDefinition.name,
}) })
@ -29,6 +39,9 @@
<p>{blockDefinition.description}</p> <p>{blockDefinition.description}</p>
</div> </div>
</div> </div>
<Modal bind:this={modal} width="30%">
<CreateWebookModal />
</Modal>
<style> <style>
.automation-block { .automation-block {
@ -62,6 +75,7 @@
} }
.automation-text p { .automation-text p {
font-size: 12px; font-size: 12px;
line-height: 1.4;
color: var(--grey-7); color: var(--grey-7);
margin: 0; margin: 0;
} }

View File

@ -1,12 +1,15 @@
<script> <script>
import TableSelector from "./ParamInputs/TableSelector.svelte" import TableSelector from "./ParamInputs/TableSelector.svelte"
import RowSelector from "./ParamInputs/RowSelector.svelte" import RowSelector from "./ParamInputs/RowSelector.svelte"
import { Input, TextArea, Select, Label } from "@budibase/bbui" import { Button, Input, TextArea, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import BindableInput from "../../userInterface/BindableInput.svelte" import BindableInput from "../../userInterface/BindableInput.svelte"
export let block export let block
export let webhookModal
$: inputs = Object.entries(block.schema?.inputs?.properties || {}) $: inputs = Object.entries(block.schema?.inputs?.properties || {})
$: stepId = block.stepId
$: bindings = getAvailableBindings( $: bindings = getAvailableBindings(
block, block,
$automationStore.selectedAutomation?.automation?.definition $automationStore.selectedAutomation?.automation?.definition
@ -64,6 +67,8 @@
<TableSelector bind:value={block.inputs[key]} /> <TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'} {:else if value.customType === 'row'}
<RowSelector bind:value={block.inputs[key]} {bindings} /> <RowSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.customType === 'webhookUrl'}
<WebhookDisplay value={block.inputs[key]} />
{:else if value.type === 'string' || value.type === 'number'} {:else if value.type === 'string' || value.type === 'number'}
<BindableInput <BindableInput
type="string" type="string"
@ -73,6 +78,11 @@
{/if} {/if}
</div> </div>
{/each} {/each}
{#if stepId === 'WEBHOOK'}
<Button wide secondary on:click={() => webhookModal.show()}>
Setup webhook
</Button>
{/if}
</div> </div>
<style> <style>

View File

@ -2,11 +2,13 @@
import { backendUiStore, automationStore } from "builderStore" import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte" import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import { Button, Input, Label } from "@budibase/bbui" import { Button, Input, Label, Modal } from "@budibase/bbui"
import CreateWebookModal from "../Shared/CreateWebhookModal.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let selectedTab = "SETUP" let selectedTab = "SETUP"
let confirmDeleteDialog let confirmDeleteDialog
let webhookModal
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation $: automation = $automationStore.selectedAutomation?.automation
@ -60,7 +62,9 @@
</header> </header>
<div class="content"> <div class="content">
{#if $automationStore.selectedBlock} {#if $automationStore.selectedBlock}
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} /> <AutomationBlockSetup
bind:block={$automationStore.selectedBlock}
{webhookModal} />
{:else if $automationStore.selectedAutomation} {:else if $automationStore.selectedAutomation}
<div class="block-label">Automation <b>{automation.name}</b></div> <div class="block-label">Automation <b>{automation.name}</b></div>
<Button secondary wide on:click={testAutomation}>Test Automation</Button> <Button secondary wide on:click={testAutomation}>Test Automation</Button>
@ -102,6 +106,9 @@
body={`Are you sure you wish to delete the automation '${name}'?`} body={`Are you sure you wish to delete the automation '${name}'?`}
okText="Delete Automation" okText="Delete Automation"
onOk={deleteAutomation} /> onOk={deleteAutomation} />
<Modal bind:this={webhookModal} width="30%">
<CreateWebookModal />
</Modal>
<style> <style>
section { section {

View File

@ -0,0 +1,110 @@
<script>
import { store, backendUiStore, automationStore } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte"
import { cloneDeep } from "lodash/fp"
import analytics from "analytics"
const POLL_RATE_MS = 2500
const DEFAULT_SCHEMA_OUTPUT = "Any input allowed"
let name
let interval
let finished = false
let schemaURL
let propCount = 0
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId
$: automation = $automationStore.selectedAutomation?.automation
onMount(async () => {
// save the automation initially
await automationStore.actions.save({
instanceId,
automation,
})
interval = setInterval(async () => {
await automationStore.actions.fetch()
const outputs = automation?.definition?.trigger.schema.outputs?.properties
// always one prop for the "body"
if (Object.keys(outputs).length > 1) {
propCount = Object.keys(outputs).length - 1
finished = true
}
}, POLL_RATE_MS)
schemaURL = automation?.definition?.trigger?.inputs.schemaUrl
})
onDestroy(() => {
clearInterval(interval)
})
</script>
<ModalContent
title="Webhook Setup"
confirmText="Finished"
showConfirmButton={finished}
cancelText="Skip">
<p>
Webhooks are for receiving data. To make them easier please use the URL
shown below and send a
<code>POST</code>
request to it from your other application. If you're unable to do this now
then you can skip this step, however we will not be able to configure
bindings for your later actions!
</p>
<WebhookDisplay value={schemaURL} />
{#if finished}
<p class="finished-text">
Request received! We found
{propCount}
bindable value{propCount > 1 ? 's' : ''}.
</p>
{/if}
<div slot="footer">
<a target="_blank" href="https://docs.budibase.com/automate/steps/triggers">
<i class="ri-information-line" />
<span>Learn about webhooks</span>
</a>
</div>
</ModalContent>
<style>
a {
color: var(--ink);
font-size: 14px;
vertical-align: middle;
display: flex;
align-items: center;
text-decoration: none;
}
a span {
text-decoration: underline;
}
i {
font-size: 20px;
margin-right: var(--spacing-m);
text-decoration: none;
}
p {
margin-top: 0;
padding-top: 0;
text-align: justify;
}
.finished-text {
font-weight: 500;
text-align: center;
color: var(--blue);
}
h5 {
margin: 0;
}
code {
padding: 1px 4px 1px 4px;
font-size: 14px;
color: var(--grey-7);
background-color: var(--grey-4);
border-radius: 2px;
}
</style>

View File

@ -0,0 +1,64 @@
<script>
import { notifier } from "builderStore/store/notifications"
import { Input } from "@budibase/bbui"
import { store } from "../../../builderStore"
export let value
export let production = false
$: appId = $store.appId
function fullWebhookURL(uri) {
if (production) {
return `https://${appId}.app.budi.live/${uri}`
} else {
return `http://localhost:4001/${uri}`
}
}
function copyToClipboard() {
const dummy = document.createElement("textarea")
document.body.appendChild(dummy)
dummy.value = fullWebhookURL(value)
dummy.select()
document.execCommand("copy")
document.body.removeChild(dummy)
notifier.success(`URL copied to clipboard`)
}
</script>
<div>
<Input disabled="true" thin value={fullWebhookURL(value)} />
<span on:click={() => copyToClipboard()}>
<i class="ri-clipboard-line copy-icon" />
</span>
</div>
<style>
div {
position: relative;
}
div :global(input:disabled) {
color: var(--grey-7);
}
span {
position: absolute;
border: none;
border-radius: 50%;
height: 24px;
width: 24px;
background: white;
right: var(--spacing-s);
bottom: 9px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
span:hover {
background-color: var(--grey-3);
}
</style>

View File

@ -0,0 +1,76 @@
<script>
import { automationStore } from "builderStore"
import { ModalContent } from "@budibase/bbui"
import { onMount } from "svelte"
import WebhookDisplay from "../automation/Shared/WebhookDisplay.svelte"
import analytics from "analytics"
let webhookUrls = []
$: automations = $automationStore.automations
onMount(() => {
webhookUrls = automations.map(automation => {
const trigger = automation.definition.trigger
if (trigger?.stepId === "WEBHOOK" && trigger.inputs) {
return {
type: "Automation",
name: automation.name,
url: trigger.inputs.triggerUrl,
}
}
})
})
</script>
<ModalContent title="Webhook Endpoints" confirmText="Done">
<p>See below the list of deployed webhook URLs.</p>
{#each webhookUrls as webhookUrl}
<div>
<h5>{webhookUrl.type} - {webhookUrl.name}</h5>
<WebhookDisplay value={webhookUrl.url} production={true} />
</div>
{/each}
{#if webhookUrls.length === 0}
<h5>No webhooks found.</h5>
{/if}
<div slot="footer">
<a target="_blank" href="https://docs.budibase.com/automate/steps/triggers">
<i class="ri-information-line" />
<span>Learn about webhooks</span>
</a>
</div>
</ModalContent>
<style>
a {
color: var(--ink);
font-size: 14px;
vertical-align: middle;
display: flex;
align-items: center;
text-decoration: none;
}
a span {
text-decoration: underline;
}
i {
font-size: 20px;
margin-right: var(--spacing-m);
text-decoration: none;
}
p {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
text-align: justify;
}
h5 {
margin-top: 0;
padding-top: 0;
}
</style>

View File

@ -2,9 +2,10 @@
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import { slide } from "svelte/transition" import { slide } from "svelte/transition"
import { Heading, Body } from "@budibase/bbui" import { Heading, Body, Button, Modal } from "@budibase/bbui"
import api from "builderStore/api" import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
const DATE_OPTIONS = { const DATE_OPTIONS = {
fullDate: { fullDate: {
@ -23,6 +24,7 @@
export let appId export let appId
let modal
let poll let poll
let deployments = [] let deployments = []
let deploymentUrl = `https://${appId}.app.budi.live/${appId}` let deploymentUrl = `https://${appId}.app.budi.live/${appId}`
@ -52,9 +54,12 @@
<section class="deployment-history" in:slide> <section class="deployment-history" in:slide>
<header> <header>
<h4>Deployment History</h4> <h4>Deployment History</h4>
<div class="deploy-div">
<a target="_blank" href={`https://${appId}.app.budi.live/${appId}`}> <a target="_blank" href={`https://${appId}.app.budi.live/${appId}`}>
View Your Deployed App → View Your Deployed App →
</a> </a>
<Button primary on:click={() => modal.show()}>View webhooks</Button>
</div>
</header> </header>
<div class="deployment-list"> <div class="deployment-list">
{#each deployments as deployment} {#each deployments as deployment}
@ -80,6 +85,9 @@
</div> </div>
</section> </section>
{/if} {/if}
<Modal bind:this={modal} width="30%">
<CreateWebhookDeploymentModal />
</Modal>
<style> <style>
.deployment:nth-child(odd) { .deployment:nth-child(odd) {
@ -99,6 +107,13 @@
header { header {
margin-left: var(--spacing-l); margin-left: var(--spacing-l);
margin-bottom: var(--spacing-xl); margin-bottom: var(--spacing-xl);
margin-right: var(--spacing-l);
}
.deploy-div {
display: flex;
justify-content: space-between;
align-items: center;
} }
.deployment-history { .deployment-history {

View File

@ -18,10 +18,21 @@
export let align export let align
export let popover = null export let popover = null
let getCaretPosition
$: categories = Object.entries(groupBy("category", bindings)) $: categories = Object.entries(groupBy("category", bindings))
function onClickBinding(binding) { function onClickBinding(binding) {
value += `{{ ${binding.path} }}` const position = getCaretPosition()
const toAdd = `{{ ${binding.path} }}`
if (position.start) {
value =
value.substring(0, position.start) +
toAdd +
value.substring(position.end, value.length)
} else {
value += toAdd
}
} }
</script> </script>
@ -54,6 +65,7 @@
</Body> </Body>
<TextArea <TextArea
thin thin
bind:getCaretPosition
bind:value bind:value
placeholder="Add options from the left, type text, or do both" /> placeholder="Add options from the left, type text, or do both" />
<div class="controls"> <div class="controls">

View File

@ -709,19 +709,19 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/bbui@^1.44.1": "@budibase/bbui@^1.47.0":
version "1.44.1" version "1.47.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.44.1.tgz#1bfca2d3a40a14eb0ba136e24afb7139694b4970" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.47.0.tgz#f7c1f1efff12b2a62eb52536fcc9a037f9b25982"
integrity sha512-joFH++mzFJZkMR3ZSv8Q8cE/qNn8o8aZGbHeO57Smiai3h2tjeTyVLuJ+yt3P1XQ8SE13epAp38kvsPosUpbkQ== integrity sha512-mWOglrEjKSOe7At2gA8HDv5MUvPzFrpGgiikAeMEulvE7sq/SCreXtAps/Jx+RKq/tUMEZkDoA3S5nuQhsNM/A==
dependencies: dependencies:
sirv-cli "^0.4.6" sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0" svelte-flatpickr "^2.4.0"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^0.2.4": "@budibase/client@^0.2.6":
version "0.2.4" version "0.2.6"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.2.4.tgz#da958faa50c59f6a9c41c692b7a19d6a6ea98bc1" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.2.6.tgz#de1b4872c7956d386a3b08969eda509bd39d1a64"
integrity sha512-MsFbWcsh3t1lyLgTb4UMccjshy6jd3A77lqs1CpXjHr+2LmXwvIriLgruycAvFrtqZzYG+dGe0rWwX0auwaaZw== integrity sha512-sSoGN0k2Tcc5GewBjFMN+G3h21O9JvakYI33kBKgEVGrdEQLBbry7vRKb+lALeW2Bz65gafZL2joZzL8vnH0lw==
dependencies: dependencies:
deep-equal "^2.0.1" deep-equal "^2.0.1"
mustache "^4.0.1" mustache "^4.0.1"

View File

@ -57,8 +57,10 @@
"electron-updater": "^4.3.1", "electron-updater": "^4.3.1",
"fix-path": "^3.0.0", "fix-path": "^3.0.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"handlebars": "^4.7.6",
"jimp": "^0.16.1", "jimp": "^0.16.1",
"joi": "^17.2.1", "joi": "^17.2.1",
"jsonschema": "^1.4.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.7.0", "koa": "^2.7.0",
"koa-body": "^4.2.0", "koa-body": "^4.2.0",
@ -77,6 +79,7 @@
"sanitize-s3-objectkey": "^0.0.1", "sanitize-s3-objectkey": "^0.0.1",
"squirrelly": "^7.5.0", "squirrelly": "^7.5.0",
"tar-fs": "^2.1.0", "tar-fs": "^2.1.0",
"to-json-schema": "^0.2.5",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"validate.js": "^0.13.1", "validate.js": "^0.13.1",
"worker-farm": "^1.7.0", "worker-farm": "^1.7.0",

View File

@ -2,8 +2,10 @@ const CouchDB = require("../../db")
const actions = require("../../automations/actions") const actions = require("../../automations/actions")
const logic = require("../../automations/logic") const logic = require("../../automations/logic")
const triggers = require("../../automations/triggers") const triggers = require("../../automations/triggers")
const webhooks = require("./webhook")
const { getAutomationParams, generateAutomationID } = require("../../db/utils") const { getAutomationParams, generateAutomationID } = require("../../db/utils")
const WH_STEP_ID = triggers.BUILTIN_DEFINITIONS.WEBHOOK.stepId
/************************* /*************************
* * * *
* BUILDER FUNCTIONS * * BUILDER FUNCTIONS *
@ -30,6 +32,67 @@ function cleanAutomationInputs(automation) {
return automation return automation
} }
/**
* This function handles checking if any webhooks need to be created or deleted for automations.
* @param {object} user The user object, including all auth info
* @param {object|undefined} oldAuto The old automation object if updating/deleting
* @param {object|undefined} newAuto The new automation object if creating/updating
* @returns {Promise<object|undefined>} After this is complete the new automation object may have been updated and should be
* written to DB (this does not write to DB as it would be wasteful to repeat).
*/
async function checkForWebhooks({ user, oldAuto, newAuto }) {
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
const newTrigger = newAuto ? newAuto.definition.trigger : null
function isWebhookTrigger(auto) {
return (
auto &&
auto.definition.trigger &&
auto.definition.trigger.stepId === WH_STEP_ID
)
}
// need to delete webhook
if (
isWebhookTrigger(oldAuto) &&
!isWebhookTrigger(newAuto) &&
oldTrigger.webhookId
) {
let db = new CouchDB(user.instanceId)
// need to get the webhook to get the rev
const webhook = await db.get(oldTrigger.webhookId)
const ctx = {
user,
params: { id: webhook._id, rev: webhook._rev },
}
// might be updating - reset the inputs to remove the URLs
if (newTrigger) {
delete newTrigger.webhookId
newTrigger.inputs = {}
}
await webhooks.destroy(ctx)
}
// need to create webhook
else if (!isWebhookTrigger(oldAuto) && isWebhookTrigger(newAuto)) {
const ctx = {
user,
request: {
body: new webhooks.Webhook(
"Automation webhook",
webhooks.WebhookType.AUTOMATION,
newAuto._id
),
},
}
await webhooks.save(ctx)
const id = ctx.body.webhook._id
newTrigger.webhookId = id
newTrigger.inputs = {
schemaUrl: `api/webhooks/schema/${user.instanceId}/${id}`,
triggerUrl: `api/webhooks/trigger/${user.instanceId}/${id}`,
}
}
return newAuto
}
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
let automation = ctx.request.body let automation = ctx.request.body
@ -39,7 +102,8 @@ exports.create = async function(ctx) {
automation.type = "automation" automation.type = "automation"
automation = cleanAutomationInputs(automation) automation = cleanAutomationInputs(automation)
const response = await db.post(automation) automation = await checkForWebhooks({ user: ctx.user, newAuto: automation })
const response = await db.put(automation)
automation._rev = response.rev automation._rev = response.rev
ctx.status = 200 ctx.status = 200
@ -56,8 +120,13 @@ exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.user.appId automation.appId = ctx.user.appId
const oldAutomation = await db.get(automation._id)
automation = cleanAutomationInputs(automation) automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({
user: ctx.user,
oldAuto: oldAutomation,
newAuto: automation,
})
const response = await db.put(automation) const response = await db.put(automation)
automation._rev = response.rev automation._rev = response.rev
@ -89,6 +158,8 @@ exports.find = async function(ctx) {
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const oldAutomation = await db.get(ctx.params.id)
await checkForWebhooks({ user: ctx.user, oldAuto: oldAutomation })
ctx.body = await db.remove(ctx.params.id, ctx.params.rev) ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
} }

View File

@ -0,0 +1,99 @@
const CouchDB = require("../../db")
const { generateWebhookID, getWebhookParams } = require("../../db/utils")
const toJsonSchema = require("to-json-schema")
const validate = require("jsonschema").validate
const triggers = require("../../automations/triggers")
const AUTOMATION_DESCRIPTION = "Generated from Webhook Schema"
function Webhook(name, type, target) {
this.live = true
this.name = name
this.action = {
type,
target,
}
}
exports.Webhook = Webhook
exports.WebhookType = {
AUTOMATION: "automation",
}
exports.fetch = async ctx => {
const db = new CouchDB(ctx.user.instanceId)
const response = await db.allDocs(
getWebhookParams(null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
}
exports.save = async ctx => {
const db = new CouchDB(ctx.user.instanceId)
const webhook = ctx.request.body
webhook.appId = ctx.user.appId
// check that the webhook exists
if (webhook._id) {
await db.get(webhook._id)
} else {
webhook._id = generateWebhookID()
}
const response = await db.put(webhook)
ctx.body = {
message: "Webhook created successfully",
webhook: {
...webhook,
...response,
},
}
}
exports.destroy = async ctx => {
const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
}
exports.buildSchema = async ctx => {
const db = new CouchDB(ctx.params.instance)
const webhook = await db.get(ctx.params.id)
webhook.bodySchema = toJsonSchema(ctx.request.body)
// update the automation outputs
if (webhook.action.type === exports.WebhookType.AUTOMATION) {
let automation = await db.get(webhook.action.target)
const autoOutputs = automation.definition.trigger.schema.outputs
let properties = webhook.bodySchema.properties
for (let prop of Object.keys(properties)) {
autoOutputs.properties[prop] = {
type: properties[prop].type,
description: AUTOMATION_DESCRIPTION,
}
}
await db.put(automation)
}
ctx.body = await db.put(webhook)
}
exports.trigger = async ctx => {
const db = new CouchDB(ctx.params.instance)
const webhook = await db.get(ctx.params.id)
// validate against the schema
if (webhook.bodySchema) {
validate(ctx.request.body, webhook.bodySchema)
}
const target = await db.get(webhook.action.target)
if (webhook.action.type === exports.WebhookType.AUTOMATION) {
// trigger with both the pure request and then expand it
// incase the user has produced a schema to bind to
await triggers.externalTrigger(target, {
body: ctx.request.body,
...ctx.request.body,
instanceId: ctx.params.instance,
})
}
ctx.status = 200
ctx.body = "Webhook trigger fired successfully"
}

View File

@ -22,6 +22,7 @@ const {
apiKeysRoutes, apiKeysRoutes,
templatesRoutes, templatesRoutes,
analyticsRoutes, analyticsRoutes,
webhookRoutes,
} = require("./routes") } = require("./routes")
const router = new Router() const router = new Router()
@ -88,6 +89,9 @@ router.use(instanceRoutes.allowedMethods())
router.use(automationRoutes.routes()) router.use(automationRoutes.routes())
router.use(automationRoutes.allowedMethods()) router.use(automationRoutes.allowedMethods())
router.use(webhookRoutes.routes())
router.use(webhookRoutes.allowedMethods())
router.use(deployRoutes.routes()) router.use(deployRoutes.routes())
router.use(deployRoutes.allowedMethods()) router.use(deployRoutes.allowedMethods())

View File

@ -2,7 +2,7 @@ const Router = require("@koa/router")
const controller = require("../controllers/automation") const controller = require("../controllers/automation")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator") const joiValidator = require("../../middleware/joi-validator")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER, EXECUTE_AUTOMATION } = require("../../utilities/accessLevels")
const Joi = require("joi") const Joi = require("joi")
const router = Router() const router = Router()
@ -33,7 +33,7 @@ function generateValidator(existing = false) {
type: Joi.string().valid("automation").required(), type: Joi.string().valid("automation").required(),
definition: Joi.object({ definition: Joi.object({
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])), steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
trigger: generateStepSchema(["TRIGGER"]), trigger: generateStepSchema(["TRIGGER"]).allow(null),
}).required().unknown(true), }).required().unknown(true),
}).unknown(true)) }).unknown(true))
} }
@ -73,7 +73,11 @@ router
generateValidator(false), generateValidator(false),
controller.create controller.create
) )
.post("/api/automations/:id/trigger", controller.trigger) .post(
"/api/automations/:id/trigger",
authorized(EXECUTE_AUTOMATION),
controller.trigger
)
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy) .delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
module.exports = router module.exports = router

View File

@ -10,6 +10,7 @@ const viewRoutes = require("./view")
const staticRoutes = require("./static") const staticRoutes = require("./static")
const componentRoutes = require("./component") const componentRoutes = require("./component")
const automationRoutes = require("./automation") const automationRoutes = require("./automation")
const webhookRoutes = require("./webhook")
const accesslevelRoutes = require("./accesslevel") const accesslevelRoutes = require("./accesslevel")
const deployRoutes = require("./deploy") const deployRoutes = require("./deploy")
const apiKeysRoutes = require("./apikeys") const apiKeysRoutes = require("./apikeys")
@ -34,4 +35,5 @@ module.exports = {
apiKeysRoutes, apiKeysRoutes,
templatesRoutes, templatesRoutes,
analyticsRoutes, analyticsRoutes,
webhookRoutes,
} }

View File

@ -0,0 +1,45 @@
const Router = require("@koa/router")
const controller = require("../controllers/webhook")
const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator")
const { BUILDER, EXECUTE_WEBHOOK } = require("../../utilities/accessLevels")
const Joi = require("joi")
const router = Router()
function generateSaveValidator() {
// prettier-ignore
return joiValidator.body(Joi.object({
live: Joi.bool(),
_id: Joi.string().optional(),
_rev: Joi.string().optional(),
name: Joi.string().required(),
bodySchema: Joi.object().optional(),
action: Joi.object({
type: Joi.string().required().valid(controller.WebhookType.AUTOMATION),
target: Joi.string().required(),
}).required(),
}).unknown(true))
}
router
.get("/api/webhooks", authorized(BUILDER), controller.fetch)
.put(
"/api/webhooks",
authorized(BUILDER),
generateSaveValidator(),
controller.save
)
.delete("/api/webhooks/:id/:rev", authorized(BUILDER), controller.destroy)
.post(
"/api/webhooks/schema/:instance/:id",
authorized(BUILDER),
controller.buildSchema
)
.post(
"/api/webhooks/trigger/:instance/:id",
authorized(EXECUTE_WEBHOOK),
controller.trigger
)
module.exports = router

View File

@ -3,6 +3,7 @@ const createRow = require("./steps/createRow")
const updateRow = require("./steps/updateRow") const updateRow = require("./steps/updateRow")
const deleteRow = require("./steps/deleteRow") const deleteRow = require("./steps/deleteRow")
const createUser = require("./steps/createUser") const createUser = require("./steps/createUser")
const outgoingWebhook = require("./steps/outgoingWebhook")
const environment = require("../environment") const environment = require("../environment")
const download = require("download") const download = require("download")
const fetch = require("node-fetch") const fetch = require("node-fetch")
@ -21,6 +22,7 @@ const BUILTIN_ACTIONS = {
UPDATE_ROW: updateRow.run, UPDATE_ROW: updateRow.run,
DELETE_ROW: deleteRow.run, DELETE_ROW: deleteRow.run,
CREATE_USER: createUser.run, CREATE_USER: createUser.run,
OUTGOING_WEBHOOK: outgoingWebhook.run,
} }
const BUILTIN_DEFINITIONS = { const BUILTIN_DEFINITIONS = {
SEND_EMAIL: sendEmail.definition, SEND_EMAIL: sendEmail.definition,
@ -28,6 +30,7 @@ const BUILTIN_DEFINITIONS = {
UPDATE_ROW: updateRow.definition, UPDATE_ROW: updateRow.definition,
DELETE_ROW: deleteRow.definition, DELETE_ROW: deleteRow.definition,
CREATE_USER: createUser.definition, CREATE_USER: createUser.definition,
OUTGOING_WEBHOOK: outgoingWebhook.definition,
} }
let AUTOMATION_BUCKET = environment.AUTOMATION_BUCKET let AUTOMATION_BUCKET = environment.AUTOMATION_BUCKET

View File

@ -6,7 +6,7 @@ const usage = require("../../utilities/usageQuota")
module.exports.definition = { module.exports.definition = {
name: "Create Row", name: "Create Row",
tagline: "Create a {{inputs.enriched.table.name}} row", tagline: "Create a {{inputs.enriched.table.name}} row",
icon: "ri-save-3-fill", icon: "ri-save-3-line",
description: "Add a row to your database", description: "Add a row to your database",
type: "ACTION", type: "ACTION",
stepId: "CREATE_ROW", stepId: "CREATE_ROW",

View File

@ -6,7 +6,7 @@ const usage = require("../../utilities/usageQuota")
module.exports.definition = { module.exports.definition = {
description: "Create a new user", description: "Create a new user",
tagline: "Create user {{inputs.username}}", tagline: "Create user {{inputs.username}}",
icon: "ri-user-add-fill", icon: "ri-user-add-line",
name: "Create User", name: "Create User",
type: "ACTION", type: "ACTION",
stepId: "CREATE_USER", stepId: "CREATE_USER",

View File

@ -2,7 +2,7 @@ let { wait } = require("../../utilities")
module.exports.definition = { module.exports.definition = {
name: "Delay", name: "Delay",
icon: "ri-time-fill", icon: "ri-time-line",
tagline: "Delay for {{inputs.time}} milliseconds", tagline: "Delay for {{inputs.time}} milliseconds",
description: "Delay the automation until an amount of time has passed", description: "Delay the automation until an amount of time has passed",
stepId: "DELAY", stepId: "DELAY",

View File

@ -0,0 +1,95 @@
const fetch = require("node-fetch")
const RequestType = {
POST: "POST",
GET: "GET",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
}
const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH]
/**
* Note, there is some functionality in this that is not currently exposed as it
* is complex and maybe better to be opinionated here.
* GET/DELETE requests cannot handle body elements so they will not be sent if configured.
*/
module.exports.definition = {
name: "Outgoing webhook",
tagline: "Send a {{inputs.requestMethod}} request",
icon: "ri-send-plane-line",
description: "Send a request of specified method to a URL",
type: "ACTION",
stepId: "OUTGOING_WEBHOOK",
inputs: {
requestMethod: "POST",
url: "http://",
requestBody: "{}",
},
schema: {
inputs: {
properties: {
requestMethod: {
type: "string",
enum: Object.values(RequestType),
title: "Request method",
},
url: {
type: "string",
title: "URL",
},
requestBody: {
type: "string",
title: "JSON Body",
customType: "wide",
},
},
required: ["requestMethod", "url"],
},
outputs: {
properties: {
response: {
type: "object",
description: "The response from the webhook",
},
success: {
type: "boolean",
description: "Whether the action was successful",
},
},
required: ["response", "success"],
},
},
}
module.exports.run = async function({ inputs }) {
let { requestMethod, url, requestBody } = inputs
if (!url.startsWith("http")) {
url = `http://${url}`
}
const request = {
method: requestMethod,
}
if (
requestBody &&
requestBody.length !== 0 &&
BODY_REQUESTS.indexOf(requestMethod) !== -1
) {
request.body = JSON.parse(requestBody)
}
try {
const response = await fetch(url, request)
return {
response: await response.json(),
success: response.status === 200,
}
} catch (err) {
return {
success: false,
response: err,
}
}
}

View File

@ -1,7 +1,7 @@
module.exports.definition = { module.exports.definition = {
description: "Send an email", description: "Send an email",
tagline: "Send email to {{inputs.to}}", tagline: "Send email to {{inputs.to}}",
icon: "ri-mail-open-fill", icon: "ri-mail-open-line",
name: "Send Email", name: "Send Email",
type: "ACTION", type: "ACTION",
stepId: "SEND_EMAIL", stepId: "SEND_EMAIL",

View File

@ -4,7 +4,7 @@ const automationUtils = require("../automationUtils")
module.exports.definition = { module.exports.definition = {
name: "Update Row", name: "Update Row",
tagline: "Update a {{inputs.enriched.table.name}} row", tagline: "Update a {{inputs.enriched.table.name}} row",
icon: "ri-refresh-fill", icon: "ri-refresh-line",
description: "Update a row in your database", description: "Update a row in your database",
type: "ACTION", type: "ACTION",
stepId: "UPDATE_ROW", stepId: "UPDATE_ROW",

View File

@ -1,8 +1,12 @@
const mustache = require("mustache") const handlebars = require("handlebars")
const actions = require("./actions") const actions = require("./actions")
const logic = require("./logic") const logic = require("./logic")
const automationUtils = require("./automationUtils") const automationUtils = require("./automationUtils")
handlebars.registerHelper("object", value => {
return new handlebars.SafeString(JSON.stringify(value))
})
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
function recurseMustache(inputs, context) { function recurseMustache(inputs, context) {
@ -10,7 +14,8 @@ function recurseMustache(inputs, context) {
let val = inputs[key] let val = inputs[key]
if (typeof val === "string") { if (typeof val === "string") {
val = automationUtils.cleanMustache(inputs[key]) val = automationUtils.cleanMustache(inputs[key])
inputs[key] = mustache.render(val, context) const template = handlebars.compile(val)
inputs[key] = template(context)
} }
// this covers objects and arrays // this covers objects and arrays
else if (typeof val === "object") { else if (typeof val === "object") {

View File

@ -83,6 +83,42 @@ const BUILTIN_DEFINITIONS = {
}, },
type: "TRIGGER", type: "TRIGGER",
}, },
WEBHOOK: {
name: "Webhook",
event: "web:trigger",
icon: "ri-global-line",
tagline: "Webhook endpoint is hit",
description: "Trigger an automation when a HTTP POST webhook is hit",
stepId: "WEBHOOK",
inputs: {},
schema: {
inputs: {
properties: {
schemaUrl: {
type: "string",
customType: "webhookUrl",
title: "Schema URL",
},
triggerUrl: {
type: "string",
customType: "webhookUrl",
title: "Trigger URL",
},
},
required: ["schemaUrl", "triggerUrl"],
},
outputs: {
properties: {
body: {
type: "object",
description: "Body of the request which hit the webhook",
},
},
required: ["body"],
},
},
type: "TRIGGER",
},
} }
async function queueRelevantRowAutomations(event, eventType) { async function queueRelevantRowAutomations(event, eventType) {

View File

@ -11,6 +11,7 @@ const DocumentTypes = {
LINK: "li", LINK: "li",
APP: "app", APP: "app",
ACCESS_LEVEL: "ac", ACCESS_LEVEL: "ac",
WEBHOOK: "wh",
} }
exports.DocumentTypes = DocumentTypes exports.DocumentTypes = DocumentTypes
@ -164,3 +165,18 @@ exports.generateAccessLevelID = () => {
exports.getAccessLevelParams = (accessLevelId = null, otherProps = {}) => { exports.getAccessLevelParams = (accessLevelId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.ACCESS_LEVEL, accessLevelId, otherProps) return getDocParams(DocumentTypes.ACCESS_LEVEL, accessLevelId, otherProps)
} }
/**
* Generates a new webhook ID.
* @returns {string} The new webhook ID which the webhook doc can be stored under.
*/
exports.generateWebhookID = () => {
return `${DocumentTypes.WEBHOOK}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving a webhook, this is a utility function for the getDocParams function.
*/
exports.getWebhookParams = (webhookId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.WEBHOOK, webhookId, otherProps)
}

View File

@ -9,7 +9,13 @@ const environment = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient") const { apiKeyTable } = require("../db/dynamoClient")
const { AuthTypes } = require("../constants") const { AuthTypes } = require("../constants")
const LOCAL_PASS = new RegExp(["webhooks/trigger", "webhooks/schema"].join("|"))
module.exports = (permName, getItemId) => async (ctx, next) => { module.exports = (permName, getItemId) => async (ctx, next) => {
// webhooks can pass locally
if (!environment.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
return next()
}
if ( if (
environment.CLOUD && environment.CLOUD &&
ctx.headers["x-api-key"] && ctx.headers["x-api-key"] &&

View File

@ -3,6 +3,7 @@ module.exports.READ_TABLE = "read-table"
module.exports.WRITE_TABLE = "write-table" module.exports.WRITE_TABLE = "write-table"
module.exports.READ_VIEW = "read-view" module.exports.READ_VIEW = "read-view"
module.exports.EXECUTE_AUTOMATION = "execute-automation" module.exports.EXECUTE_AUTOMATION = "execute-automation"
module.exports.EXECUTE_WEBHOOK = "execute-webhook"
module.exports.USER_MANAGEMENT = "user-management" module.exports.USER_MANAGEMENT = "user-management"
module.exports.BUILDER = "builder" module.exports.BUILDER = "builder"
module.exports.LIST_USERS = "list-users" module.exports.LIST_USERS = "list-users"

View File

@ -1352,6 +1352,13 @@ axios@^0.19.2:
dependencies: dependencies:
follow-redirects "1.5.10" follow-redirects "1.5.10"
axios@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
dependencies:
follow-redirects "^1.10.0"
babel-jest@^24.9.0: babel-jest@^24.9.0:
version "24.9.0" version "24.9.0"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54"
@ -3178,6 +3185,11 @@ follow-redirects@1.5.10:
dependencies: dependencies:
debug "=3.1.0" debug "=3.1.0"
follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
for-in@^1.0.2: for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -3482,6 +3494,18 @@ growly@^1.3.0:
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
handlebars@^4.7.6:
version "4.7.6"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
har-schema@^2.0.0: har-schema@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -4714,6 +4738,11 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
jsonschema@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2"
integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==
jsonwebtoken@^8.5.1: jsonwebtoken@^8.5.1:
version "8.5.1" version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@ -5160,6 +5189,21 @@ lodash.isstring@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.keys@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205"
integrity sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.omit@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
integrity sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=
lodash.once@^4.0.0: lodash.once@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@ -5175,6 +5219,16 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash.without@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
lodash.xor@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.xor/-/lodash.xor-4.5.0.tgz#4d48ed7e98095b0632582ba714d3ff8ae8fb1db6"
integrity sha1-TUjtfpgJWwYyWCunFNP/iuj7HbY=
lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.3: lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.3:
version "4.17.20" version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
@ -5469,6 +5523,11 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo-async@^2.6.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
nice-try@^1.0.4: nice-try@^1.0.4:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -7471,6 +7530,18 @@ to-fast-properties@^2.0.0:
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
to-json-schema@^0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/to-json-schema/-/to-json-schema-0.2.5.tgz#ef3c3f11ad64460dcfbdbafd0fd525d69d62a98f"
integrity sha512-jP1ievOee8pec3tV9ncxLSS48Bnw7DIybgy112rhMCEhf3K4uyVNZZHr03iQQBzbV5v5Hos+dlZRRyk6YSMNDw==
dependencies:
lodash.isequal "^4.5.0"
lodash.keys "^4.2.0"
lodash.merge "^4.6.2"
lodash.omit "^4.5.0"
lodash.without "^4.4.0"
lodash.xor "^4.5.0"
to-object-path@^0.3.0: to-object-path@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
@ -7632,6 +7703,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
uglify-js@^3.1.4:
version "3.11.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.4.tgz#b47b7ae99d4bd1dca65b53aaa69caa0909e6fadf"
integrity sha512-FyYnoxVL1D6+jDGQpbK5jW6y/2JlVfRfEeQ67BPCUg5wfCjaKOpr2XeceE4QL+MkhxliLtf5EbrMDZgzpt2CNw==
unbzip2-stream@^1.0.9: unbzip2-stream@^1.0.9:
version "1.4.3" version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
@ -7942,6 +8018,11 @@ word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
worker-farm@^1.7.0: worker-farm@^1.7.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"