mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
68 changed files with 1466 additions and 2195 deletions
File diff suppressed because it is too large
@ -0,0 +1,46 @@ |
|||
const { MetadataTypes } = require("../../constants") |
|||
const CouchDB = require("../../db") |
|||
const { generateMetadataID } = require("../../db/utils") |
|||
const { saveEntityMetadata, deleteEntityMetadata } = require("../../utilities") |
|||
|
|||
exports.getTypes = async ctx => { |
|||
ctx.body = { |
|||
types: MetadataTypes, |
|||
} |
|||
} |
|||
|
|||
exports.saveMetadata = async ctx => { |
|||
const { type, entityId } = ctx.params |
|||
if (type === MetadataTypes.AUTOMATION_TEST_HISTORY) { |
|||
ctx.throw(400, "Cannot save automation history type") |
|||
} |
|||
ctx.body = await saveEntityMetadata( |
|||
ctx.appId, |
|||
type, |
|||
entityId, |
|||
ctx.request.body |
|||
) |
|||
} |
|||
|
|||
exports.deleteMetadata = async ctx => { |
|||
const { type, entityId } = ctx.params |
|||
await deleteEntityMetadata(ctx.appId, type, entityId) |
|||
ctx.body = { |
|||
message: "Metadata deleted successfully", |
|||
} |
|||
} |
|||
|
|||
exports.getMetadata = async ctx => { |
|||
const { type, entityId } = ctx.params |
|||
const db = new CouchDB(ctx.appId) |
|||
const id = generateMetadataID(type, entityId) |
|||
try { |
|||
ctx.body = await db.get(id) |
|||
} catch (err) { |
|||
if (err.status === 404) { |
|||
ctx.body = {} |
|||
} else { |
|||
ctx.throw(err.status, err) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../controllers/metadata") |
|||
const { |
|||
middleware: appInfoMiddleware, |
|||
AppType, |
|||
} = require("../../middleware/appInfo") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("@budibase/auth/permissions") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.post( |
|||
"/api/metadata/:type/:entityId", |
|||
authorized(BUILDER), |
|||
appInfoMiddleware({ appType: AppType.DEV }), |
|||
controller.saveMetadata |
|||
) |
|||
.delete( |
|||
"/api/metadata/:type/:entityId", |
|||
authorized(BUILDER), |
|||
appInfoMiddleware({ appType: AppType.DEV }), |
|||
controller.deleteMetadata |
|||
) |
|||
.get( |
|||
"/api/metadata/type", |
|||
authorized(BUILDER), |
|||
appInfoMiddleware({ appType: AppType.DEV }), |
|||
controller.getTypes |
|||
) |
|||
.get( |
|||
"/api/metadata/:type/:entityId", |
|||
authorized(BUILDER), |
|||
appInfoMiddleware({ appType: AppType.DEV }), |
|||
controller.getMetadata |
|||
) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,65 @@ |
|||
const { testAutomation } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
const { MetadataTypes } = require("../../../constants") |
|||
|
|||
describe("/metadata", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let automation |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
automation = await config.createAutomation() |
|||
}) |
|||
|
|||
async function createMetadata(data, type = MetadataTypes.AUTOMATION_TEST_INPUT) { |
|||
const res = await request |
|||
.post(`/api/metadata/${type}/${automation._id}`) |
|||
.send(data) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body._rev).toBeDefined() |
|||
} |
|||
|
|||
async function getMetadata(type) { |
|||
const res = await request |
|||
.get(`/api/metadata/${type}/${automation._id}`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
return res.body |
|||
} |
|||
|
|||
describe("save", () => { |
|||
it("should be able to save some metadata", async () => { |
|||
await createMetadata({ test: "a" }) |
|||
const testInput = await getMetadata(MetadataTypes.AUTOMATION_TEST_INPUT) |
|||
expect(testInput.test).toBe("a") |
|||
}) |
|||
|
|||
it("should save history metadata on automation run", async () => { |
|||
// this should have created some history
|
|||
await testAutomation(config, automation) |
|||
const metadata = await getMetadata(MetadataTypes.AUTOMATION_TEST_HISTORY) |
|||
expect(metadata).toBeDefined() |
|||
expect(metadata.history.length).toBe(1) |
|||
expect(typeof metadata.history[0].occurredAt).toBe("number") |
|||
}) |
|||
}) |
|||
|
|||
describe("destroy", () => { |
|||
it("should be able to delete some test inputs", async () => { |
|||
const res = await request |
|||
.delete(`/api/metadata/${MetadataTypes.AUTOMATION_TEST_INPUT}/${automation._id}`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.message).toBeDefined() |
|||
const metadata = await getMetadata(MetadataTypes.AUTOMATION_TEST_INPUT) |
|||
expect(metadata.test).toBeUndefined() |
|||
}) |
|||
}) |
|||
}) |
|||
@ -1,51 +1,17 @@ |
|||
const triggers = require("./triggers") |
|||
const actions = require("./actions") |
|||
const env = require("../environment") |
|||
const workerFarm = require("worker-farm") |
|||
const singleThread = require("./thread") |
|||
const { getAPIKey, update, Properties } = require("../utilities/usageQuota") |
|||
|
|||
let workers = workerFarm(require.resolve("./thread")) |
|||
|
|||
function runWorker(job) { |
|||
return new Promise((resolve, reject) => { |
|||
workers(job, err => { |
|||
if (err) { |
|||
reject(err) |
|||
} else { |
|||
resolve() |
|||
} |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
async function updateQuota(automation) { |
|||
const appId = automation.appId |
|||
const apiObj = await getAPIKey(appId) |
|||
// this will fail, causing automation to escape if limits reached
|
|||
await update(apiObj.apiKey, Properties.AUTOMATION, 1) |
|||
return apiObj.apiKey |
|||
} |
|||
const { processEvent } = require("./utils") |
|||
const { queue } = require("./bullboard") |
|||
|
|||
/** |
|||
* This module is built purely to kick off the worker farm and manage the inputs/outputs |
|||
*/ |
|||
module.exports.init = async function () { |
|||
await actions.init() |
|||
triggers.automationQueue.process(async job => { |
|||
try { |
|||
if (env.USE_QUOTAS) { |
|||
job.data.automation.apiKey = await updateQuota(job.data.automation) |
|||
} |
|||
if (env.isProd()) { |
|||
await runWorker(job) |
|||
} else { |
|||
await singleThread(job) |
|||
} |
|||
} catch (err) { |
|||
console.error( |
|||
`${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` |
|||
) |
|||
} |
|||
exports.init = function () { |
|||
// this promise will not complete
|
|||
return queue.process(async job => { |
|||
await processEvent(job) |
|||
}) |
|||
} |
|||
|
|||
exports.getQueues = () => { |
|||
return [queue] |
|||
} |
|||
exports.queue = queue |
|||
|
|||
@ -1,20 +0,0 @@ |
|||
let filter = require("./steps/filter") |
|||
let delay = require("./steps/delay") |
|||
|
|||
let BUILTIN_LOGIC = { |
|||
DELAY: delay.run, |
|||
FILTER: filter.run, |
|||
} |
|||
|
|||
let BUILTIN_DEFINITIONS = { |
|||
DELAY: delay.definition, |
|||
FILTER: filter.definition, |
|||
} |
|||
|
|||
module.exports.getLogic = function (logicName) { |
|||
if (BUILTIN_LOGIC[logicName] != null) { |
|||
return BUILTIN_LOGIC[logicName] |
|||
} |
|||
} |
|||
|
|||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS |
|||
@ -0,0 +1,83 @@ |
|||
const fetch = require("node-fetch") |
|||
const { getFetchResponse } = require("./utils") |
|||
|
|||
const DEFAULT_USERNAME = "Budibase Automate" |
|||
const DEFAULT_AVATAR_URL = "https://i.imgur.com/a1cmTKM.png" |
|||
|
|||
exports.definition = { |
|||
name: "Discord Message", |
|||
tagline: "Send a message to a Discord server", |
|||
description: "Send a message to a Discord server", |
|||
icon: "ri-discord-line", |
|||
stepId: "discord", |
|||
type: "ACTION", |
|||
internal: false, |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
url: { |
|||
type: "string", |
|||
title: "Discord Webhook URL", |
|||
}, |
|||
username: { |
|||
type: "string", |
|||
title: "Bot Name", |
|||
}, |
|||
avatar_url: { |
|||
type: "string", |
|||
title: "Bot Avatar URL", |
|||
}, |
|||
content: { |
|||
type: "string", |
|||
title: "Message", |
|||
}, |
|||
}, |
|||
required: ["url", "content"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
httpStatus: { |
|||
type: "number", |
|||
description: "The HTTP status code of the request", |
|||
}, |
|||
response: { |
|||
type: "string", |
|||
description: "The response from the Discord Webhook", |
|||
}, |
|||
success: { |
|||
type: "boolean", |
|||
description: "Whether the message sent successfully", |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
exports.run = async function ({ inputs }) { |
|||
let { url, username, avatar_url, content } = inputs |
|||
if (!username) { |
|||
username = DEFAULT_USERNAME |
|||
} |
|||
if (!avatar_url) { |
|||
avatar_url = DEFAULT_AVATAR_URL |
|||
} |
|||
const response = await fetch(url, { |
|||
method: "post", |
|||
body: JSON.stringify({ |
|||
username, |
|||
avatar_url, |
|||
content, |
|||
}), |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
}) |
|||
|
|||
const { status, message } = await getFetchResponse(response) |
|||
return { |
|||
httpStatus: status, |
|||
success: status === 200, |
|||
response: message, |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
const fetch = require("node-fetch") |
|||
const { getFetchResponse } = require("./utils") |
|||
|
|||
exports.definition = { |
|||
name: "Integromat Integration", |
|||
tagline: "Trigger an Integromat scenario", |
|||
description: |
|||
"Performs a webhook call to Integromat and gets the response (if configured)", |
|||
icon: "ri-shut-down-line", |
|||
stepId: "integromat", |
|||
type: "ACTION", |
|||
internal: false, |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
url: { |
|||
type: "string", |
|||
title: "Webhook URL", |
|||
}, |
|||
value1: { |
|||
type: "string", |
|||
title: "Input Value 1", |
|||
}, |
|||
value2: { |
|||
type: "string", |
|||
title: "Input Value 2", |
|||
}, |
|||
value3: { |
|||
type: "string", |
|||
title: "Input Value 3", |
|||
}, |
|||
value4: { |
|||
type: "string", |
|||
title: "Input Value 4", |
|||
}, |
|||
value5: { |
|||
type: "string", |
|||
title: "Input Value 5", |
|||
}, |
|||
}, |
|||
required: ["url", "value1", "value2", "value3", "value4", "value5"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
success: { |
|||
type: "boolean", |
|||
description: "Whether call was successful", |
|||
}, |
|||
httpStatus: { |
|||
type: "number", |
|||
description: "The HTTP status code returned", |
|||
}, |
|||
response: { |
|||
type: "object", |
|||
description: "The webhook response - this can have properties", |
|||
}, |
|||
}, |
|||
required: ["success", "response"], |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
exports.run = async function ({ inputs }) { |
|||
const { url, value1, value2, value3, value4, value5 } = inputs |
|||
|
|||
const response = await fetch(url, { |
|||
method: "post", |
|||
body: JSON.stringify({ |
|||
value1, |
|||
value2, |
|||
value3, |
|||
value4, |
|||
value5, |
|||
}), |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
}) |
|||
|
|||
const { status, message } = await getFetchResponse(response) |
|||
return { |
|||
httpStatus: status, |
|||
success: status === 200, |
|||
response: message, |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
module.exports.definition = { |
|||
description: "Send an email using SendGrid", |
|||
tagline: "Send email to {{inputs.to}}", |
|||
icon: "ri-mail-open-line", |
|||
name: "Send Email (SendGrid)", |
|||
type: "ACTION", |
|||
stepId: "SEND_EMAIL", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
apiKey: { |
|||
type: "string", |
|||
title: "SendGrid API key", |
|||
}, |
|||
to: { |
|||
type: "string", |
|||
title: "Send To", |
|||
}, |
|||
from: { |
|||
type: "string", |
|||
title: "Send From", |
|||
}, |
|||
subject: { |
|||
type: "string", |
|||
title: "Email Subject", |
|||
}, |
|||
contents: { |
|||
type: "string", |
|||
title: "Email Contents", |
|||
}, |
|||
}, |
|||
required: ["to", "from", "subject", "contents"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
success: { |
|||
type: "boolean", |
|||
description: "Whether the email was sent", |
|||
}, |
|||
response: { |
|||
type: "object", |
|||
description: "A response from the email client, this may be an error", |
|||
}, |
|||
}, |
|||
required: ["success"], |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
module.exports.run = async function ({ inputs }) { |
|||
const sgMail = require("@sendgrid/mail") |
|||
sgMail.setApiKey(inputs.apiKey) |
|||
const msg = { |
|||
to: inputs.to, |
|||
from: inputs.from, |
|||
subject: inputs.subject, |
|||
text: inputs.contents ? inputs.contents : "Empty", |
|||
} |
|||
|
|||
try { |
|||
let response = await sgMail.send(msg) |
|||
return { |
|||
success: true, |
|||
response, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
response: err, |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
const fetch = require("node-fetch") |
|||
const { getFetchResponse } = require("./utils") |
|||
|
|||
exports.definition = { |
|||
name: "Slack Message", |
|||
tagline: "Send a message to Slack", |
|||
description: "Send a message to Slack", |
|||
icon: "ri-slack-line", |
|||
stepId: "slack", |
|||
type: "ACTION", |
|||
internal: false, |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
url: { |
|||
type: "string", |
|||
title: "Incoming Webhook URL", |
|||
}, |
|||
text: { |
|||
type: "string", |
|||
title: "Message", |
|||
}, |
|||
}, |
|||
required: ["url", "text"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
httpStatus: { |
|||
type: "number", |
|||
description: "The HTTP status code of the request", |
|||
}, |
|||
success: { |
|||
type: "boolean", |
|||
description: "Whether the message sent successfully", |
|||
}, |
|||
response: { |
|||
type: "string", |
|||
description: "The response from the Slack Webhook", |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
exports.run = async function ({ inputs }) { |
|||
let { url, text } = inputs |
|||
const response = await fetch(url, { |
|||
method: "post", |
|||
body: JSON.stringify({ |
|||
text, |
|||
}), |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
}) |
|||
|
|||
const { status, message } = await getFetchResponse(response) |
|||
return { |
|||
httpStatus: status, |
|||
response: message, |
|||
success: status === 200, |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
exports.getFetchResponse = async fetched => { |
|||
let status = fetched.status, |
|||
message |
|||
const contentType = fetched.headers.get("content-type") |
|||
try { |
|||
if (contentType && contentType.indexOf("application/json") !== -1) { |
|||
message = await fetched.json() |
|||
} else { |
|||
message = await fetched.text() |
|||
} |
|||
} catch (err) { |
|||
message = "Failed to retrieve response" |
|||
} |
|||
return { status, message } |
|||
} |
|||
|
|||
// need to make sure all ctx structures have the
|
|||
// throw added to them, so that controllers don't
|
|||
// throw a ctx.throw undefined when error occurs
|
|||
exports.buildCtx = (appId, emitter, { body, params } = {}) => { |
|||
const ctx = { |
|||
appId, |
|||
user: { appId }, |
|||
eventEmitter: emitter, |
|||
throw: (code, error) => { |
|||
throw error |
|||
}, |
|||
} |
|||
if (body) { |
|||
ctx.request = { body } |
|||
} |
|||
if (params) { |
|||
ctx.params = params |
|||
} |
|||
return ctx |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
const fetch = require("node-fetch") |
|||
const { getFetchResponse } = require("./utils") |
|||
|
|||
exports.definition = { |
|||
name: "Zapier Webhook", |
|||
stepId: "zapier", |
|||
type: "ACTION", |
|||
internal: false, |
|||
description: "Trigger a Zapier Zap via webhooks", |
|||
tagline: "Trigger a Zapier Zap", |
|||
icon: "ri-flashlight-line", |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
url: { |
|||
type: "string", |
|||
title: "Webhook URL", |
|||
}, |
|||
value1: { |
|||
type: "string", |
|||
title: "Payload Value 1", |
|||
}, |
|||
value2: { |
|||
type: "string", |
|||
title: "Payload Value 2", |
|||
}, |
|||
value3: { |
|||
type: "string", |
|||
title: "Payload Value 3", |
|||
}, |
|||
value4: { |
|||
type: "string", |
|||
title: "Payload Value 4", |
|||
}, |
|||
value5: { |
|||
type: "string", |
|||
title: "Payload Value 5", |
|||
}, |
|||
}, |
|||
required: ["url"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
httpStatus: { |
|||
type: "number", |
|||
description: "The HTTP status code of the request", |
|||
}, |
|||
response: { |
|||
type: "string", |
|||
description: "The response from Zapier", |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
exports.run = async function ({ inputs }) { |
|||
const { url, value1, value2, value3, value4, value5 } = inputs |
|||
|
|||
// send the platform to make sure zaps always work, even
|
|||
// if no values supplied
|
|||
const response = await fetch(url, { |
|||
method: "post", |
|||
body: JSON.stringify({ |
|||
platform: "budibase", |
|||
value1, |
|||
value2, |
|||
value3, |
|||
value4, |
|||
value5, |
|||
}), |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
}) |
|||
|
|||
const { status, message } = await getFetchResponse(response) |
|||
|
|||
return { |
|||
success: status === 200, |
|||
httpStatus: status, |
|||
response: message, |
|||
} |
|||
} |
|||
@ -1,48 +1,48 @@ |
|||
const setup = require("./utilities") |
|||
const { LogicConditions } = require("../steps/filter") |
|||
const { FilterConditions } = require("../steps/filter") |
|||
|
|||
describe("test the filter logic", () => { |
|||
async function checkFilter(field, condition, value, pass = true) { |
|||
let res = await setup.runStep(setup.logic.FILTER.stepId, |
|||
let res = await setup.runStep(setup.actions.FILTER.stepId, |
|||
{ field, condition, value } |
|||
) |
|||
expect(res.success).toEqual(pass) |
|||
} |
|||
|
|||
it("should be able test equality", async () => { |
|||
await checkFilter("hello", LogicConditions.EQUAL, "hello", true) |
|||
await checkFilter("hello", LogicConditions.EQUAL, "no", false) |
|||
await checkFilter("hello", FilterConditions.EQUAL, "hello", true) |
|||
await checkFilter("hello", FilterConditions.EQUAL, "no", false) |
|||
}) |
|||
|
|||
it("should be able to test greater than", async () => { |
|||
await checkFilter(10, LogicConditions.GREATER_THAN, 5, true) |
|||
await checkFilter(10, LogicConditions.GREATER_THAN, 15, false) |
|||
await checkFilter(10, FilterConditions.GREATER_THAN, 5, true) |
|||
await checkFilter(10, FilterConditions.GREATER_THAN, 15, false) |
|||
}) |
|||
|
|||
it("should be able to test less than", async () => { |
|||
await checkFilter(5, LogicConditions.LESS_THAN, 10, true) |
|||
await checkFilter(15, LogicConditions.LESS_THAN, 10, false) |
|||
await checkFilter(5, FilterConditions.LESS_THAN, 10, true) |
|||
await checkFilter(15, FilterConditions.LESS_THAN, 10, false) |
|||
}) |
|||
|
|||
it("should be able to in-equality", async () => { |
|||
await checkFilter("hello", LogicConditions.NOT_EQUAL, "no", true) |
|||
await checkFilter(10, LogicConditions.NOT_EQUAL, 10, false) |
|||
await checkFilter("hello", FilterConditions.NOT_EQUAL, "no", true) |
|||
await checkFilter(10, FilterConditions.NOT_EQUAL, 10, false) |
|||
}) |
|||
|
|||
it("check number coercion", async () => { |
|||
await checkFilter("10", LogicConditions.GREATER_THAN, "5", true) |
|||
await checkFilter("10", FilterConditions.GREATER_THAN, "5", true) |
|||
}) |
|||
|
|||
it("check date coercion", async () => { |
|||
await checkFilter( |
|||
(new Date()).toISOString(), |
|||
LogicConditions.GREATER_THAN, |
|||
FilterConditions.GREATER_THAN, |
|||
(new Date(-10000)).toISOString(), |
|||
true |
|||
) |
|||
}) |
|||
|
|||
it("check objects always false", async () => { |
|||
await checkFilter({}, LogicConditions.EQUAL, {}, false) |
|||
await checkFilter({}, FilterConditions.EQUAL, {}, false) |
|||
}) |
|||
}) |
|||
@ -1,36 +0,0 @@ |
|||
const setup = require("./utilities") |
|||
|
|||
jest.mock("@sendgrid/mail") |
|||
|
|||
describe("test the send email action", () => { |
|||
let inputs |
|||
let config = setup.getConfig() |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
inputs = { |
|||
to: "me@test.com", |
|||
from: "budibase@test.com", |
|||
subject: "Testing", |
|||
text: "Email contents", |
|||
} |
|||
}) |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
it("should be able to run the action", async () => { |
|||
const res = await setup.runStep(setup.actions.SEND_EMAIL.stepId, inputs) |
|||
expect(res.success).toEqual(true) |
|||
// the mocked module throws back the input
|
|||
expect(res.response.to).toEqual("me@test.com") |
|||
}) |
|||
|
|||
it("should return an error if input an invalid email address", async () => { |
|||
const res = await setup.runStep(setup.actions.SEND_EMAIL.stepId, { |
|||
...inputs, |
|||
to: "invalid@test.com", |
|||
}) |
|||
expect(res.success).toEqual(false) |
|||
}) |
|||
|
|||
}) |
|||
@ -0,0 +1,31 @@ |
|||
exports.definition = { |
|||
name: "App Action", |
|||
event: "app:trigger", |
|||
icon: "ri-window-fill", |
|||
tagline: "Automation fired from the frontend", |
|||
description: "Trigger an automation from an action inside your app", |
|||
stepId: "APP", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
fields: { |
|||
type: "object", |
|||
customType: "triggerSchema", |
|||
title: "Fields", |
|||
}, |
|||
}, |
|||
required: [], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
fields: { |
|||
type: "object", |
|||
description: "Fields submitted from the app frontend", |
|||
}, |
|||
}, |
|||
required: ["fields"], |
|||
}, |
|||
}, |
|||
type: "TRIGGER", |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
exports.definition = { |
|||
name: "Cron Trigger", |
|||
event: "cron:trigger", |
|||
icon: "ri-timer-line", |
|||
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)", |
|||
description: "Triggers automation on a cron schedule.", |
|||
stepId: "CRON", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
cron: { |
|||
type: "string", |
|||
customType: "cron", |
|||
title: "Expression", |
|||
}, |
|||
}, |
|||
required: ["cron"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
timestamp: { |
|||
type: "number", |
|||
description: "Timestamp the cron was executed", |
|||
}, |
|||
}, |
|||
required: ["timestamp"], |
|||
}, |
|||
}, |
|||
type: "TRIGGER", |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
const app = require("./app") |
|||
const cron = require("./cron") |
|||
const rowDeleted = require("./rowDeleted") |
|||
const rowSaved = require("./rowSaved") |
|||
const rowUpdated = require("./rowUpdated") |
|||
const webhook = require("./webhook") |
|||
|
|||
exports.definitions = { |
|||
ROW_SAVED: rowSaved.definition, |
|||
ROW_UPDATED: rowUpdated.definition, |
|||
ROW_DELETED: rowDeleted.definition, |
|||
WEBHOOK: webhook.definition, |
|||
APP: app.definition, |
|||
CRON: cron.definition, |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
exports.definition = { |
|||
name: "Row Deleted", |
|||
event: "row:delete", |
|||
icon: "ri-delete-bin-line", |
|||
tagline: "Row is deleted from {{inputs.enriched.table.name}}", |
|||
description: "Fired when a row is deleted from your database", |
|||
stepId: "ROW_DELETED", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
tableId: { |
|||
type: "string", |
|||
customType: "table", |
|||
title: "Table", |
|||
}, |
|||
}, |
|||
required: ["tableId"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
row: { |
|||
type: "object", |
|||
customType: "row", |
|||
description: "The row that was deleted", |
|||
}, |
|||
}, |
|||
required: ["row"], |
|||
}, |
|||
}, |
|||
type: "TRIGGER", |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
exports.definition = { |
|||
name: "Row Created", |
|||
event: "row:save", |
|||
icon: "ri-save-line", |
|||
tagline: "Row is added to {{inputs.enriched.table.name}}", |
|||
description: "Fired when a row is added to your database", |
|||
stepId: "ROW_SAVED", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
tableId: { |
|||
type: "string", |
|||
customType: "table", |
|||
title: "Table", |
|||
}, |
|||
}, |
|||
required: ["tableId"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
row: { |
|||
type: "object", |
|||
customType: "row", |
|||
description: "The new row that was created", |
|||
}, |
|||
id: { |
|||
type: "string", |
|||
description: "Row ID - can be used for updating", |
|||
}, |
|||
revision: { |
|||
type: "string", |
|||
description: "Revision of row", |
|||
}, |
|||
}, |
|||
required: ["row", "id"], |
|||
}, |
|||
}, |
|||
type: "TRIGGER", |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
exports.definition = { |
|||
name: "Row Updated", |
|||
event: "row:update", |
|||
icon: "ri-refresh-line", |
|||
tagline: "Row is updated in {{inputs.enriched.table.name}}", |
|||
description: "Fired when a row is updated in your database", |
|||
stepId: "ROW_UPDATED", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
tableId: { |
|||
type: "string", |
|||
customType: "table", |
|||
title: "Table", |
|||
}, |
|||
}, |
|||
required: ["tableId"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
row: { |
|||
type: "object", |
|||
customType: "row", |
|||
description: "The row that was updated", |
|||
}, |
|||
id: { |
|||
type: "string", |
|||
description: "Row ID - can be used for updating", |
|||
}, |
|||
revision: { |
|||
type: "string", |
|||
description: "Revision of row", |
|||
}, |
|||
}, |
|||
required: ["row", "id"], |
|||
}, |
|||
}, |
|||
type: "TRIGGER", |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
exports.definition = { |
|||
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", |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
const runner = require("./thread") |
|||
const { definitions } = require("./triggerInfo") |
|||
const webhooks = require("../api/controllers/webhook") |
|||
const CouchDB = require("../db") |
|||
const { queue } = require("./bullboard") |
|||
const newid = require("../db/newid") |
|||
const { updateEntityMetadata } = require("../utilities") |
|||
const { MetadataTypes } = require("../constants") |
|||
|
|||
const WH_STEP_ID = definitions.WEBHOOK.stepId |
|||
const CRON_STEP_ID = definitions.CRON.stepId |
|||
|
|||
exports.processEvent = async job => { |
|||
try { |
|||
// need to actually await these so that an error can be captured properly
|
|||
return await runner(job) |
|||
} catch (err) { |
|||
console.error( |
|||
`${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` |
|||
) |
|||
return { err } |
|||
} |
|||
} |
|||
|
|||
exports.updateTestHistory = async (appId, automation, history) => { |
|||
return updateEntityMetadata( |
|||
appId, |
|||
MetadataTypes.AUTOMATION_TEST_HISTORY, |
|||
automation._id, |
|||
metadata => { |
|||
if (metadata && Array.isArray(metadata.history)) { |
|||
metadata.history.push(history) |
|||
} else { |
|||
metadata = { |
|||
history: [history], |
|||
} |
|||
} |
|||
return metadata |
|||
} |
|||
) |
|||
} |
|||
|
|||
// end the repetition and the job itself
|
|||
exports.disableAllCrons = async appId => { |
|||
const promises = [] |
|||
const jobs = await queue.getRepeatableJobs() |
|||
for (let job of jobs) { |
|||
if (job.key.includes(`${appId}_cron`)) { |
|||
promises.push(queue.removeRepeatableByKey(job.key)) |
|||
promises.push(queue.removeJobs(job.id)) |
|||
} |
|||
} |
|||
return Promise.all(promises) |
|||
} |
|||
|
|||
/** |
|||
* This function handles checking of any cron jobs that need to be enabled/updated. |
|||
* @param {string} appId The ID of the app in which we are checking for webhooks |
|||
* @param {object|undefined} automation The automation object to be updated. |
|||
*/ |
|||
exports.enableCronTrigger = async (appId, automation) => { |
|||
const trigger = automation ? automation.definition.trigger : null |
|||
function isCronTrigger(auto) { |
|||
return ( |
|||
auto && |
|||
auto.definition.trigger && |
|||
auto.definition.trigger.stepId === CRON_STEP_ID |
|||
) |
|||
} |
|||
// need to create cron job
|
|||
if (isCronTrigger(automation)) { |
|||
// make a job id rather than letting Bull decide, makes it easier to handle on way out
|
|||
const jobId = `${appId}_cron_${newid()}` |
|||
const job = await queue.add( |
|||
{ |
|||
automation, |
|||
event: { appId, timestamp: Date.now() }, |
|||
}, |
|||
{ repeat: { cron: trigger.inputs.cron }, jobId } |
|||
) |
|||
// Assign cron job ID from bull so we can remove it later if the cron trigger is removed
|
|||
trigger.cronJobId = job.id |
|||
const db = new CouchDB(appId) |
|||
const response = await db.put(automation) |
|||
automation._id = response.id |
|||
automation._rev = response.rev |
|||
} |
|||
return automation |
|||
} |
|||
|
|||
/** |
|||
* This function handles checking if any webhooks need to be created or deleted for automations. |
|||
* @param {string} appId The ID of the app in which we are checking for webhooks |
|||
* @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). |
|||
*/ |
|||
exports.checkForWebhooks = async ({ appId, oldAuto, newAuto }) => { |
|||
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null |
|||
const newTrigger = newAuto ? newAuto.definition.trigger : null |
|||
const triggerChanged = |
|||
oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id |
|||
function isWebhookTrigger(auto) { |
|||
return ( |
|||
auto && |
|||
auto.definition.trigger && |
|||
auto.definition.trigger.stepId === WH_STEP_ID |
|||
) |
|||
} |
|||
// need to delete webhook
|
|||
if ( |
|||
isWebhookTrigger(oldAuto) && |
|||
(!isWebhookTrigger(newAuto) || triggerChanged) && |
|||
oldTrigger.webhookId |
|||
) { |
|||
try { |
|||
let db = new CouchDB(appId) |
|||
// need to get the webhook to get the rev
|
|||
const webhook = await db.get(oldTrigger.webhookId) |
|||
const ctx = { |
|||
appId, |
|||
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) |
|||
} catch (err) { |
|||
// don't worry about not being able to delete, if it doesn't exist all good
|
|||
} |
|||
} |
|||
// need to create webhook
|
|||
if ( |
|||
(!isWebhookTrigger(oldAuto) || triggerChanged) && |
|||
isWebhookTrigger(newAuto) |
|||
) { |
|||
const ctx = { |
|||
appId, |
|||
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/${appId}/${id}`, |
|||
triggerUrl: `api/webhooks/trigger/${appId}/${id}`, |
|||
} |
|||
} |
|||
return newAuto |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
const { isDevAppID, isProdAppID } = require("../db/utils") |
|||
|
|||
exports.AppType = { |
|||
DEV: "dev", |
|||
PROD: "prod", |
|||
} |
|||
|
|||
exports.middleware = |
|||
({ appType } = {}) => |
|||
(ctx, next) => { |
|||
const appId = ctx.appId |
|||
if (appType === exports.AppType.DEV && appId && !isDevAppID(appId)) { |
|||
ctx.throw(400, "Only apps in development support this endpoint") |
|||
} |
|||
if (appType === exports.AppType.PROD && appId && !isProdAppID(appId)) { |
|||
ctx.throw(400, "Only apps in production support this endpoint") |
|||
} |
|||
return next() |
|||
} |
|||
Loading…
Reference in new issue