Merge branch 'feature/page-refactor' of github.com:Budibase/budibase into feature/page-refactor

This commit is contained in:
Martin McKeaveney 2020-12-08 11:12:27 +00:00
commit 1a226214e9
10 changed files with 141 additions and 45 deletions

View File

@ -126,8 +126,8 @@ describe("/automations", () => {
trigger.id = "wadiawdo34" trigger.id = "wadiawdo34"
let createAction = ACTION_DEFINITIONS["CREATE_ROW"] let createAction = ACTION_DEFINITIONS["CREATE_ROW"]
createAction.inputs.row = { createAction.inputs.row = {
name: "{{trigger.name}}", name: "{{trigger.row.name}}",
description: "{{trigger.description}}" description: "{{trigger.row.description}}"
} }
createAction.id = "awde444wk" createAction.id = "awde444wk"
@ -166,19 +166,20 @@ describe("/automations", () => {
TEST_AUTOMATION.definition.trigger.inputs.tableId = table._id TEST_AUTOMATION.definition.trigger.inputs.tableId = table._id
TEST_AUTOMATION.definition.steps[0].inputs.row.tableId = table._id TEST_AUTOMATION.definition.steps[0].inputs.row.tableId = table._id
await createAutomation() await createAutomation()
await delay(500)
const res = await triggerWorkflow(automation._id)
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to // this looks a bit mad but we don't actually have a way to wait for a response from the automation to
// know that it has finished all of its actions - this is currently the best way // know that it has finished all of its actions - this is currently the best way
// also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works // also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works
// TODO: update when workflow logs are a thing // TODO: update when workflow logs are a thing
for (let tries = 0; tries < MAX_RETRIES; tries++) { for (let tries = 0; tries < MAX_RETRIES; tries++) {
const res = await triggerWorkflow(automation._id)
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`) expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name) expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
await delay(500) await delay(500)
let elements = await getAllFromTable(request, appId, table._id) let elements = await getAllFromTable(request, appId, table._id)
// don't test it unless there are values to test // don't test it unless there are values to test
if (elements.length === 1) { if (elements.length > 1) {
expect(elements.length).toEqual(1) expect(elements.length).toEqual(5)
expect(elements[0].name).toEqual("Test") expect(elements[0].name).toEqual("Test")
expect(elements[0].description).toEqual("TEST") expect(elements[0].description).toEqual("TEST")
return return

View File

@ -58,7 +58,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, appId, apiKey }) { module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
// TODO: better logging of when actions are missed due to missing parameters // TODO: better logging of when actions are missed due to missing parameters
if (inputs.row == null || inputs.row.tableId == null) { if (inputs.row == null || inputs.row.tableId == null) {
return return
@ -77,6 +77,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) {
body: inputs.row, body: inputs.row,
}, },
user: { appId }, user: { appId },
eventEmitter: emitter,
} }
try { try {

View File

@ -59,7 +59,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, appId, apiKey }) { module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
const { email, password, roleId } = inputs const { email, password, roleId } = inputs
const ctx = { const ctx = {
user: { user: {
@ -68,6 +68,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) {
request: { request: {
body: { email, password, roleId }, body: { email, password, roleId },
}, },
eventEmitter: emitter,
} }
try { try {

View File

@ -50,7 +50,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, appId, apiKey }) { module.exports.run = async function({ inputs, appId, apiKey, emitter }) {
// TODO: better logging of when actions are missed due to missing parameters // TODO: better logging of when actions are missed due to missing parameters
if (inputs.id == null || inputs.revision == null) { if (inputs.id == null || inputs.revision == null) {
return return
@ -62,6 +62,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) {
revId: inputs.revision, revId: inputs.revision,
}, },
user: { appId }, user: { appId },
eventEmitter: emitter,
} }
try { try {

View File

@ -53,7 +53,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, appId }) { module.exports.run = async function({ inputs, appId, emitter }) {
if (inputs.rowId == null || inputs.row == null) { if (inputs.rowId == null || inputs.row == null) {
return return
} }
@ -79,6 +79,7 @@ module.exports.run = async function({ inputs, appId }) {
body: inputs.row, body: inputs.row,
}, },
user: { appId }, user: { appId },
eventEmitter: emitter,
} }
try { try {

View File

@ -1,10 +1,31 @@
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")
const { recurseMustache } = require("../utilities/mustache") const AutomationEmitter = require("../events/AutomationEmitter")
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) {
for (let key of Object.keys(inputs)) {
let val = inputs[key]
if (typeof val === "string") {
val = automationUtils.cleanMustache(inputs[key])
const template = handlebars.compile(val)
inputs[key] = template(context)
}
// this covers objects and arrays
else if (typeof val === "object") {
inputs[key] = recurseMustache(inputs[key], context)
}
}
return inputs
}
/** /**
* The automation orchestrator is a class responsible for executing automations. * The automation orchestrator is a class responsible for executing automations.
* It handles the context of the automation and makes sure each step gets the correct * It handles the context of the automation and makes sure each step gets the correct
@ -12,12 +33,18 @@ const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
*/ */
class Orchestrator { class Orchestrator {
constructor(automation, triggerOutput) { constructor(automation, triggerOutput) {
this._metadata = triggerOutput.metadata
this._chainCount = this._metadata ? this._metadata.automationChainCount : 0
this._appId = triggerOutput.appId this._appId = triggerOutput.appId
// remove from context // remove from context
delete triggerOutput.appId delete triggerOutput.appId
delete triggerOutput.metadata
// step zero is never used as the mustache is zero indexed for customer facing // step zero is never used as the mustache is zero indexed for customer facing
this._context = { steps: [{}], trigger: triggerOutput } this._context = { steps: [{}], trigger: triggerOutput }
this._automation = automation this._automation = automation
// create an emitter which has the chain count for this automation run in it, so it can block
// excessive chaining if required
this._emitter = new AutomationEmitter(this._chainCount + 1)
} }
async getStepFunctionality(type, stepId) { async getStepFunctionality(type, stepId) {
@ -48,6 +75,7 @@ class Orchestrator {
inputs: step.inputs, inputs: step.inputs,
appId: this._appId, appId: this._appId,
apiKey: automation.apiKey, apiKey: automation.apiKey,
emitter: this._emitter,
}) })
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break break
@ -76,4 +104,4 @@ module.exports = async (job, cb = null) => {
cb(err) cb(err)
} }
} }
} }

View File

@ -174,22 +174,20 @@ async function fillRowOutput(automation, params) {
let table = await db.get(tableId) let table = await db.get(tableId)
let row = {} let row = {}
for (let schemaKey of Object.keys(table.schema)) { for (let schemaKey of Object.keys(table.schema)) {
if (params[schemaKey] != null) { const paramValue = params[schemaKey]
continue
}
let propSchema = table.schema[schemaKey] let propSchema = table.schema[schemaKey]
switch (propSchema.constraints.type) { switch (propSchema.constraints.type) {
case "string": case "string":
row[schemaKey] = FAKE_STRING row[schemaKey] = paramValue || FAKE_STRING
break break
case "boolean": case "boolean":
row[schemaKey] = FAKE_BOOL row[schemaKey] = paramValue || FAKE_BOOL
break break
case "number": case "number":
row[schemaKey] = FAKE_NUMBER row[schemaKey] = paramValue || FAKE_NUMBER
break break
case "datetime": case "datetime":
row[schemaKey] = FAKE_DATETIME row[schemaKey] = paramValue || FAKE_DATETIME
break break
} }
} }

View File

@ -0,0 +1,51 @@
const { rowEmission, tableEmission } = require("./utils")
const mainEmitter = require("./index")
// max number of automations that can chain on top of each other
const MAX_AUTOMATION_CHAIN = 5
/**
* Special emitter which takes the count of automation runs which have occurred and blocks an
* automation from running if it has reached the maximum number of chained automations runs.
* This essentially "fakes" the normal emitter to add some functionality in-between to stop automations
* from getting stuck endlessly chaining.
*/
class AutomationEmitter {
constructor(chainCount) {
this.chainCount = chainCount
this.metadata = {
automationChainCount: chainCount,
}
}
emitRow(eventName, appId, row, table = null) {
// don't emit even if we've reached max automation chain
if (this.chainCount >= MAX_AUTOMATION_CHAIN) {
return
}
rowEmission({
emitter: mainEmitter,
eventName,
appId,
row,
table,
metadata: this.metadata,
})
}
emitTable(eventName, appId, table = null) {
// don't emit even if we've reached max automation chain
if (this.chainCount > MAX_AUTOMATION_CHAIN) {
return
}
tableEmission({
emitter: mainEmitter,
eventName,
appId,
table,
metadata: this.metadata,
})
}
}
module.exports = AutomationEmitter

View File

@ -1,4 +1,5 @@
const EventEmitter = require("events").EventEmitter const EventEmitter = require("events").EventEmitter
const { rowEmission, tableEmission } = require("./utils")
/** /**
* keeping event emitter in one central location as it might be used for things other than * keeping event emitter in one central location as it might be used for things other than
@ -12,36 +13,11 @@ const EventEmitter = require("events").EventEmitter
*/ */
class BudibaseEmitter extends EventEmitter { class BudibaseEmitter extends EventEmitter {
emitRow(eventName, appId, row, table = null) { emitRow(eventName, appId, row, table = null) {
let event = { rowEmission({ emitter: this, eventName, appId, row, table })
row,
appId,
tableId: row.tableId,
}
if (table) {
event.table = table
}
event.id = row._id
if (row._rev) {
event.revision = row._rev
}
this.emit(eventName, event)
} }
emitTable(eventName, appId, table = null) { emitTable(eventName, appId, table = null) {
const tableId = table._id tableEmission({ emitter: this, eventName, appId, table })
let event = {
table: {
...table,
tableId: tableId,
},
appId,
tableId: tableId,
}
event.id = tableId
if (table._rev) {
event.revision = table._rev
}
this.emit(eventName, event)
} }
} }

View File

@ -0,0 +1,38 @@
exports.rowEmission = ({ emitter, eventName, appId, row, table, metadata }) => {
let event = {
row,
appId,
tableId: row.tableId,
}
if (table) {
event.table = table
}
event.id = row._id
if (row._rev) {
event.revision = row._rev
}
if (metadata) {
event.metadata = metadata
}
emitter.emit(eventName, event)
}
exports.tableEmission = ({ emitter, eventName, appId, table, metadata }) => {
const tableId = table._id
let event = {
table: {
...table,
tableId: tableId,
},
appId,
tableId: tableId,
}
event.id = tableId
if (table._rev) {
event.revision = table._rev
}
if (metadata) {
event.metadata = metadata
}
emitter.emit(eventName, event)
}