forked from tsai/budibase
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 { processEvent } = require("./utils") |
||||
const actions = require("./actions") |
const { queue } = require("./bullboard") |
||||
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 |
|
||||
} |
|
||||
|
|
||||
/** |
/** |
||||
* This module is built purely to kick off the worker farm and manage the inputs/outputs |
* This module is built purely to kick off the worker farm and manage the inputs/outputs |
||||
*/ |
*/ |
||||
module.exports.init = async function () { |
exports.init = function () { |
||||
await actions.init() |
// this promise will not complete
|
||||
triggers.automationQueue.process(async job => { |
return queue.process(async job => { |
||||
try { |
await processEvent(job) |
||||
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.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 setup = require("./utilities") |
||||
const { LogicConditions } = require("../steps/filter") |
const { FilterConditions } = require("../steps/filter") |
||||
|
|
||||
describe("test the filter logic", () => { |
describe("test the filter logic", () => { |
||||
async function checkFilter(field, condition, value, pass = true) { |
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 } |
{ field, condition, value } |
||||
) |
) |
||||
expect(res.success).toEqual(pass) |
expect(res.success).toEqual(pass) |
||||
} |
} |
||||
|
|
||||
it("should be able test equality", async () => { |
it("should be able test equality", async () => { |
||||
await checkFilter("hello", LogicConditions.EQUAL, "hello", true) |
await checkFilter("hello", FilterConditions.EQUAL, "hello", true) |
||||
await checkFilter("hello", LogicConditions.EQUAL, "no", false) |
await checkFilter("hello", FilterConditions.EQUAL, "no", false) |
||||
}) |
}) |
||||
|
|
||||
it("should be able to test greater than", async () => { |
it("should be able to test greater than", async () => { |
||||
await checkFilter(10, LogicConditions.GREATER_THAN, 5, true) |
await checkFilter(10, FilterConditions.GREATER_THAN, 5, true) |
||||
await checkFilter(10, LogicConditions.GREATER_THAN, 15, false) |
await checkFilter(10, FilterConditions.GREATER_THAN, 15, false) |
||||
}) |
}) |
||||
|
|
||||
it("should be able to test less than", async () => { |
it("should be able to test less than", async () => { |
||||
await checkFilter(5, LogicConditions.LESS_THAN, 10, true) |
await checkFilter(5, FilterConditions.LESS_THAN, 10, true) |
||||
await checkFilter(15, LogicConditions.LESS_THAN, 10, false) |
await checkFilter(15, FilterConditions.LESS_THAN, 10, false) |
||||
}) |
}) |
||||
|
|
||||
it("should be able to in-equality", async () => { |
it("should be able to in-equality", async () => { |
||||
await checkFilter("hello", LogicConditions.NOT_EQUAL, "no", true) |
await checkFilter("hello", FilterConditions.NOT_EQUAL, "no", true) |
||||
await checkFilter(10, LogicConditions.NOT_EQUAL, 10, false) |
await checkFilter(10, FilterConditions.NOT_EQUAL, 10, false) |
||||
}) |
}) |
||||
|
|
||||
it("check number coercion", async () => { |
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 () => { |
it("check date coercion", async () => { |
||||
await checkFilter( |
await checkFilter( |
||||
(new Date()).toISOString(), |
(new Date()).toISOString(), |
||||
LogicConditions.GREATER_THAN, |
FilterConditions.GREATER_THAN, |
||||
(new Date(-10000)).toISOString(), |
(new Date(-10000)).toISOString(), |
||||
true |
true |
||||
) |
) |
||||
}) |
}) |
||||
|
|
||||
it("check objects always false", async () => { |
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