Updating file structure so that each built in step has its own file containing the definition and the function of it, with the intention of keeping definitions together as they will be handled in the async actions.
This commit is contained in:
parent
7ef5d8a9b8
commit
1ab787afd7
|
@ -1,7 +1,8 @@
|
||||||
const CouchDB = require("../../../db")
|
const CouchDB = require("../../db")
|
||||||
const newid = require("../../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
const blockDefinitions = require("./blockDefinitions")
|
const actions = require("../../workflows/actions")
|
||||||
const triggers = require("../../../workflows/triggers")
|
const logic = require("../../workflows/logic")
|
||||||
|
const triggers = require("../../workflows/triggers")
|
||||||
|
|
||||||
/*************************
|
/*************************
|
||||||
* *
|
* *
|
||||||
|
@ -67,22 +68,22 @@ exports.destroy = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getActionList = async function(ctx) {
|
exports.getActionList = async function(ctx) {
|
||||||
ctx.body = blockDefinitions.ACTION
|
ctx.body = actions.BUILTIN_DEFINITIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTriggerList = async function(ctx) {
|
exports.getTriggerList = async function(ctx) {
|
||||||
ctx.body = blockDefinitions.TRIGGER
|
ctx.body = triggers.BUILTIN_DEFINITIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getLogicList = async function(ctx) {
|
exports.getLogicList = async function(ctx) {
|
||||||
ctx.body = blockDefinitions.LOGIC
|
ctx.body = logic.BUILTIN_DEFINITIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getDefinitionList = async function(ctx) {
|
module.exports.getDefinitionList = async function(ctx) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
logic: blockDefinitions.LOGIC,
|
logic: logic.BUILTIN_DEFINITIONS,
|
||||||
trigger: blockDefinitions.TRIGGER,
|
trigger: triggers.BUILTIN_DEFINITIONS,
|
||||||
action: blockDefinitions.ACTION,
|
action: actions.BUILTIN_DEFINITIONS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,330 +0,0 @@
|
||||||
let accessLevels = require("../../../utilities/accessLevels")
|
|
||||||
let conditions = require("../../../workflows/logic").LogicConditions
|
|
||||||
|
|
||||||
const ACTION = {
|
|
||||||
SAVE_RECORD: {
|
|
||||||
name: "Save Record",
|
|
||||||
tagline: "Save a {{inputs.enriched.model.name}} record",
|
|
||||||
icon: "ri-save-3-fill",
|
|
||||||
description: "Save a record to your database",
|
|
||||||
type: "ACTION",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
record: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
modelId: {
|
|
||||||
type: "string",
|
|
||||||
customType: "model",
|
|
||||||
title: "Table",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
customType: "record",
|
|
||||||
title: "Table",
|
|
||||||
default: {},
|
|
||||||
required: ["modelId"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["record"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
response: {
|
|
||||||
type: "object",
|
|
||||||
description: "The response from the table",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether the action was successful",
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
description: "The identifier of the new record",
|
|
||||||
},
|
|
||||||
revision: {
|
|
||||||
type: "string",
|
|
||||||
description: "The revision of the new record",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["success", "id", "revision"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DELETE_RECORD: {
|
|
||||||
description: "Delete a record from your database",
|
|
||||||
icon: "ri-delete-bin-line",
|
|
||||||
name: "Delete Record",
|
|
||||||
tagline: "Delete a {{inputs.enriched.model.name}} record",
|
|
||||||
type: "ACTION",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
modelId: {
|
|
||||||
type: "string",
|
|
||||||
customType: "model",
|
|
||||||
title: "Table",
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
title: "Record ID",
|
|
||||||
},
|
|
||||||
revision: {
|
|
||||||
type: "string",
|
|
||||||
title: "Record Revision",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["modelId", "id", "revision"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
record: {
|
|
||||||
type: "object",
|
|
||||||
customType: "record",
|
|
||||||
description: "The deleted record",
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
type: "object",
|
|
||||||
description: "The response from the table",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether the action was successful",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["record", "success"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CREATE_USER: {
|
|
||||||
description: "Create a new user",
|
|
||||||
tagline: "Create user {{inputs.username}}",
|
|
||||||
icon: "ri-user-add-fill",
|
|
||||||
name: "Create User",
|
|
||||||
type: "ACTION",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
username: {
|
|
||||||
type: "string",
|
|
||||||
title: "Username",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: "string",
|
|
||||||
customType: "password",
|
|
||||||
title: "Password",
|
|
||||||
},
|
|
||||||
accessLevelId: {
|
|
||||||
type: "string",
|
|
||||||
title: "Access Level",
|
|
||||||
default: accessLevels.POWERUSER_LEVEL_ID,
|
|
||||||
enum: accessLevels.ACCESS_LEVELS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["username", "password", "accessLevelId"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
id: {
|
|
||||||
type: "string",
|
|
||||||
description: "The identifier of the new user",
|
|
||||||
},
|
|
||||||
revision: {
|
|
||||||
type: "string",
|
|
||||||
description: "The revision of the new user",
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
type: "object",
|
|
||||||
description: "The response from the user table",
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether the action was successful",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["id", "revision", "success"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SEND_EMAIL: {
|
|
||||||
description: "Send an email.",
|
|
||||||
tagline: "Send email to {{inputs.to}}",
|
|
||||||
icon: "ri-mail-open-fill",
|
|
||||||
name: "Send Email",
|
|
||||||
type: "ACTION",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOGIC = {
|
|
||||||
FILTER: {
|
|
||||||
name: "Filter",
|
|
||||||
tagline: "{{inputs.filter}} {{inputs.condition}} {{inputs.value}}",
|
|
||||||
icon: "ri-git-branch-line",
|
|
||||||
description: "Filter any workflows which do not meet certain conditions",
|
|
||||||
type: "LOGIC",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
filter: {
|
|
||||||
type: "string",
|
|
||||||
title: "Reference Value",
|
|
||||||
},
|
|
||||||
condition: {
|
|
||||||
type: "string",
|
|
||||||
title: "Condition",
|
|
||||||
enum: conditions,
|
|
||||||
default: "equals",
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: "string",
|
|
||||||
title: "Comparison Value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["filter", "condition", "value"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
success: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether the logic block passed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["success"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DELAY: {
|
|
||||||
name: "Delay",
|
|
||||||
icon: "ri-time-fill",
|
|
||||||
tagline: "Delay for {{inputs.time}} milliseconds",
|
|
||||||
description: "Delay the workflow until an amount of time has passed",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
time: {
|
|
||||||
type: "number",
|
|
||||||
title: "Delay in milliseconds",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["time"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "LOGIC",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRIGGER = {
|
|
||||||
RECORD_SAVED: {
|
|
||||||
name: "Record Saved",
|
|
||||||
event: "record:save",
|
|
||||||
icon: "ri-save-line",
|
|
||||||
tagline: "Record is added to {{inputs.enriched.model.name}}",
|
|
||||||
description: "Fired when a record is saved to your database",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
modelId: {
|
|
||||||
type: "string",
|
|
||||||
customType: "model",
|
|
||||||
title: "Table",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["modelId"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
record: {
|
|
||||||
type: "object",
|
|
||||||
customType: "record",
|
|
||||||
description: "The new record that was saved",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["record"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "TRIGGER",
|
|
||||||
},
|
|
||||||
RECORD_DELETED: {
|
|
||||||
name: "Record Deleted",
|
|
||||||
event: "record:delete",
|
|
||||||
icon: "ri-delete-bin-line",
|
|
||||||
tagline: "Record is deleted from {{inputs.enriched.model.name}}",
|
|
||||||
description: "Fired when a record is deleted from your database",
|
|
||||||
inputs: {},
|
|
||||||
schema: {
|
|
||||||
inputs: {
|
|
||||||
properties: {
|
|
||||||
modelId: {
|
|
||||||
type: "string",
|
|
||||||
customType: "model",
|
|
||||||
title: "Table",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["modelId"],
|
|
||||||
},
|
|
||||||
outputs: {
|
|
||||||
properties: {
|
|
||||||
record: {
|
|
||||||
type: "object",
|
|
||||||
customType: "record",
|
|
||||||
description: "The record that was deleted",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["record"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: "TRIGGER",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// This contains the definitions for the steps and triggers that make up a workflow, a workflow comprises
|
|
||||||
// of many steps and a single trigger
|
|
||||||
module.exports = {
|
|
||||||
ACTION,
|
|
||||||
LOGIC,
|
|
||||||
TRIGGER,
|
|
||||||
}
|
|
|
@ -1,115 +1,20 @@
|
||||||
const userController = require("../api/controllers/user")
|
const sendEmail = require("./steps/sendEmail")
|
||||||
const recordController = require("../api/controllers/record")
|
const saveRecord = require("./steps/saveRecord")
|
||||||
const sgMail = require("@sendgrid/mail")
|
const deleteRecord = require("./steps/deleteRecord")
|
||||||
|
const createUser = require("./steps/createUser")
|
||||||
|
|
||||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
const BUILTIN_ACTIONS = {
|
||||||
|
SEND_EMAIL: sendEmail.run,
|
||||||
|
SAVE_RECORD: saveRecord.run,
|
||||||
|
DELETE_RECORD: deleteRecord.run,
|
||||||
|
CREATE_USER: createUser.run,
|
||||||
|
}
|
||||||
|
|
||||||
let BUILTIN_ACTIONS = {
|
const BUILTIN_DEFINITIONS = {
|
||||||
CREATE_USER: async function(inputs) {
|
SEND_EMAIL: sendEmail.definition,
|
||||||
const { username, password, accessLevelId } = inputs
|
SAVE_RECORD: saveRecord.definition,
|
||||||
const ctx = {
|
DELETE_RECORD: deleteRecord.definition,
|
||||||
user: {
|
CREATE_USER: createUser.definition,
|
||||||
instanceId: inputs.instanceId,
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
body: { username, password, accessLevelId },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await userController.create(ctx)
|
|
||||||
return {
|
|
||||||
response: ctx.body,
|
|
||||||
id: ctx.body._id,
|
|
||||||
revision: ctx.body._rev,
|
|
||||||
success: ctx.status === 200,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
response: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SAVE_RECORD: async function(inputs) {
|
|
||||||
const ctx = {
|
|
||||||
params: {
|
|
||||||
instanceId: inputs.instanceId,
|
|
||||||
modelId: inputs.model._id,
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
body: inputs.record,
|
|
||||||
},
|
|
||||||
user: { instanceId: inputs.instanceId },
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await recordController.save(ctx)
|
|
||||||
return {
|
|
||||||
response: ctx.body,
|
|
||||||
id: ctx.body._id,
|
|
||||||
revision: ctx.body._rev,
|
|
||||||
success: ctx.status === 200,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
response: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SEND_EMAIL: async function(inputs) {
|
|
||||||
const msg = {
|
|
||||||
to: inputs.to,
|
|
||||||
from: inputs.from,
|
|
||||||
subject: inputs.subject,
|
|
||||||
text: inputs.text,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sgMail.send(msg)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
response: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DELETE_RECORD: async function(inputs) {
|
|
||||||
const { model, ...record } = inputs.record
|
|
||||||
// TODO: better logging of when actions are missed due to missing parameters
|
|
||||||
if (record.recordId == null || record.revId == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let ctx = {
|
|
||||||
params: {
|
|
||||||
modelId: model._id,
|
|
||||||
recordId: record.recordId,
|
|
||||||
revId: record.revId,
|
|
||||||
},
|
|
||||||
user: { instanceId: inputs.instanceId },
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await recordController.destroy(ctx)
|
|
||||||
return {
|
|
||||||
response: ctx.body,
|
|
||||||
success: ctx.status === 200,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
response: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getAction = async function(actionName) {
|
module.exports.getAction = async function(actionName) {
|
||||||
|
@ -118,3 +23,5 @@ module.exports.getAction = async function(actionName) {
|
||||||
}
|
}
|
||||||
// TODO: load async actions here
|
// TODO: load async actions here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||||
|
|
|
@ -1,31 +1,20 @@
|
||||||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
let filter = require("./steps/filter")
|
||||||
|
let delay = require("./steps/delay")
|
||||||
|
|
||||||
let LOGIC = {
|
let BUILTIN_LOGIC = {
|
||||||
DELAY: async function delay(inputs) {
|
DELAY: delay.run,
|
||||||
await wait(inputs.time)
|
FILTER: filter.run,
|
||||||
},
|
}
|
||||||
|
|
||||||
FILTER: async function filter(inputs) {
|
let BUILTIN_DEFINITIONS = {
|
||||||
const { field, condition, value } = inputs
|
DELAY: delay.definition,
|
||||||
switch (condition) {
|
FILTER: filter.definition,
|
||||||
case "equals":
|
|
||||||
if (field !== value) return
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getLogic = function(logicName) {
|
module.exports.getLogic = function(logicName) {
|
||||||
if (LOGIC[logicName] != null) {
|
if (BUILTIN_LOGIC[logicName] != null) {
|
||||||
return LOGIC[logicName]
|
return BUILTIN_LOGIC[logicName]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.LogicConditions = [
|
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||||
"Equals",
|
|
||||||
"Not equals",
|
|
||||||
"Greater than",
|
|
||||||
"Less than",
|
|
||||||
]
|
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
const userController = require("../../api/controllers/user")
|
||||||
|
let accessLevels = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
description: "Create a new user",
|
||||||
|
tagline: "Create user {{inputs.username}}",
|
||||||
|
icon: "ri-user-add-fill",
|
||||||
|
name: "Create User",
|
||||||
|
type: "ACTION",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
username: {
|
||||||
|
type: "string",
|
||||||
|
title: "Username",
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: "string",
|
||||||
|
customType: "password",
|
||||||
|
title: "Password",
|
||||||
|
},
|
||||||
|
accessLevelId: {
|
||||||
|
type: "string",
|
||||||
|
title: "Access Level",
|
||||||
|
default: accessLevels.POWERUSER_LEVEL_ID,
|
||||||
|
enum: accessLevels.ACCESS_LEVELS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["username", "password", "accessLevelId"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The identifier of the new user",
|
||||||
|
},
|
||||||
|
revision: {
|
||||||
|
type: "string",
|
||||||
|
description: "The revision of the new user",
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
type: "object",
|
||||||
|
description: "The response from the user table",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the action was successful",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["id", "revision", "success"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function({ inputs, instanceId }) {
|
||||||
|
const { username, password, accessLevelId } = inputs
|
||||||
|
const ctx = {
|
||||||
|
user: {
|
||||||
|
instanceId: instanceId,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
body: { username, password, accessLevelId },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userController.create(ctx)
|
||||||
|
return {
|
||||||
|
response: ctx.body,
|
||||||
|
id: ctx.body._id,
|
||||||
|
revision: ctx.body._rev,
|
||||||
|
success: ctx.status === 200,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
name: "Delay",
|
||||||
|
icon: "ri-time-fill",
|
||||||
|
tagline: "Delay for {{inputs.time}} milliseconds",
|
||||||
|
description: "Delay the workflow until an amount of time has passed",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
time: {
|
||||||
|
type: "number",
|
||||||
|
title: "Delay in milliseconds",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["time"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "LOGIC",
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function delay({ inputs }) {
|
||||||
|
await wait(inputs.time)
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
const recordController = require("../../api/controllers/record")
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
description: "Delete a record from your database",
|
||||||
|
icon: "ri-delete-bin-line",
|
||||||
|
name: "Delete Record",
|
||||||
|
tagline: "Delete a {{inputs.enriched.model.name}} record",
|
||||||
|
type: "ACTION",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
modelId: {
|
||||||
|
type: "string",
|
||||||
|
customType: "model",
|
||||||
|
title: "Table",
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
title: "Record ID",
|
||||||
|
},
|
||||||
|
revision: {
|
||||||
|
type: "string",
|
||||||
|
title: "Record Revision",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["modelId", "id", "revision"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
customType: "record",
|
||||||
|
description: "The deleted record",
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
type: "object",
|
||||||
|
description: "The response from the table",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the action was successful",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["record", "success"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function({ inputs, instanceId }) {
|
||||||
|
// TODO: better logging of when actions are missed due to missing parameters
|
||||||
|
if (inputs.id == null || inputs.revision == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let ctx = {
|
||||||
|
params: {
|
||||||
|
modelId: inputs.modelId,
|
||||||
|
recordId: inputs.id,
|
||||||
|
revId: inputs.revision,
|
||||||
|
},
|
||||||
|
user: { instanceId },
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recordController.destroy(ctx)
|
||||||
|
return {
|
||||||
|
response: ctx.body,
|
||||||
|
success: ctx.status === 200,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
const LogicConditions = {
|
||||||
|
EQUALS: "Equals",
|
||||||
|
NOT_EQUALS: "Not equals",
|
||||||
|
GREATER_THAN: "Greater than",
|
||||||
|
LESS_THAN: "Less than",
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
name: "Filter",
|
||||||
|
tagline: "{{inputs.filter}} {{inputs.condition}} {{inputs.value}}",
|
||||||
|
icon: "ri-git-branch-line",
|
||||||
|
description: "Filter any workflows which do not meet certain conditions",
|
||||||
|
type: "LOGIC",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: "string",
|
||||||
|
title: "Reference Value",
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
type: "string",
|
||||||
|
title: "Condition",
|
||||||
|
enum: Object.values(LogicConditions),
|
||||||
|
default: "equals",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: "string",
|
||||||
|
title: "Comparison Value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["filter", "condition", "value"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the logic block passed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["success"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function filter({ inputs }) {
|
||||||
|
const { field, condition, value } = inputs
|
||||||
|
let success
|
||||||
|
if (typeof field !== "object" && typeof value !== "object") {
|
||||||
|
switch (condition) {
|
||||||
|
case LogicConditions.EQUALS:
|
||||||
|
success = field === value
|
||||||
|
break
|
||||||
|
case LogicConditions.NOT_EQUALS:
|
||||||
|
success = field !== value
|
||||||
|
break
|
||||||
|
case LogicConditions.GREATER_THAN:
|
||||||
|
success = field > value
|
||||||
|
break
|
||||||
|
case LogicConditions.LESS_THAN:
|
||||||
|
success = field < value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
return { success }
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
const recordController = require("../../api/controllers/record")
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
name: "Save Record",
|
||||||
|
tagline: "Save a {{inputs.enriched.model.name}} record",
|
||||||
|
icon: "ri-save-3-fill",
|
||||||
|
description: "Save a record to your database",
|
||||||
|
type: "ACTION",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
modelId: {
|
||||||
|
type: "string",
|
||||||
|
customType: "model",
|
||||||
|
title: "Table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customType: "record",
|
||||||
|
title: "The record to be written",
|
||||||
|
default: {},
|
||||||
|
required: ["modelId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["record"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
response: {
|
||||||
|
type: "object",
|
||||||
|
description: "The response from the table",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the action was successful",
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The identifier of the new record",
|
||||||
|
},
|
||||||
|
revision: {
|
||||||
|
type: "string",
|
||||||
|
description: "The revision of the new record",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["success", "id", "revision"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function({ inputs, instanceId }) {
|
||||||
|
// TODO: better logging of when actions are missed due to missing parameters
|
||||||
|
if (inputs.record == null || inputs.record.modelId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// have to clean up the record, remove the model from it
|
||||||
|
const ctx = {
|
||||||
|
params: {
|
||||||
|
modelId: inputs.record.modelId,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
body: inputs.record,
|
||||||
|
},
|
||||||
|
user: { instanceId },
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recordController.save(ctx)
|
||||||
|
return {
|
||||||
|
response: ctx.body,
|
||||||
|
id: ctx.body._id,
|
||||||
|
revision: ctx.body._rev,
|
||||||
|
success: ctx.status === 200,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
const sgMail = require("@sendgrid/mail")
|
||||||
|
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
description: "Send an email.",
|
||||||
|
tagline: "Send email to {{inputs.to}}",
|
||||||
|
icon: "ri-mail-open-fill",
|
||||||
|
name: "Send Email",
|
||||||
|
type: "ACTION",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
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 msg = {
|
||||||
|
to: inputs.to,
|
||||||
|
from: inputs.from,
|
||||||
|
subject: inputs.subject,
|
||||||
|
text: inputs.text,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response = await sgMail.send(msg)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
response,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,18 +2,34 @@ const mustache = require("mustache")
|
||||||
const actions = require("./actions")
|
const actions = require("./actions")
|
||||||
const logic = require("./logic")
|
const logic = require("./logic")
|
||||||
|
|
||||||
|
function recurseMustache(inputs, context) {
|
||||||
|
for (let key in Object.keys(inputs)) {
|
||||||
|
let val = inputs[key]
|
||||||
|
if (typeof val === "string") {
|
||||||
|
inputs[key] = mustache.render(val, { context })
|
||||||
|
}
|
||||||
|
// this covers objects and arrays
|
||||||
|
else if (typeof val === "object") {
|
||||||
|
inputs[key] = recurseMustache(inputs[key], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflow orchestrator is a class responsible for executing workflows.
|
* The workflow orchestrator is a class responsible for executing workflows.
|
||||||
* It handles the context of the workflow and makes sure each step gets the correct
|
* It handles the context of the workflow and makes sure each step gets the correct
|
||||||
* inputs and handles any outputs.
|
* inputs and handles any outputs.
|
||||||
*/
|
*/
|
||||||
class Orchestrator {
|
class Orchestrator {
|
||||||
constructor(workflow) {
|
constructor(workflow, triggerOutput) {
|
||||||
this._context = {}
|
this._instanceId = triggerOutput.instanceId
|
||||||
|
// block zero is never used as the mustache is zero indexed for customer facing
|
||||||
|
this._context = { blocks: [{}, triggerOutput] }
|
||||||
this._workflow = workflow
|
this._workflow = workflow
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStep(type, stepId) {
|
async getStepFunctionality(type, stepId) {
|
||||||
let step = null
|
let step = null
|
||||||
if (type === "ACTION") {
|
if (type === "ACTION") {
|
||||||
step = await actions.getAction(stepId)
|
step = await actions.getAction(stepId)
|
||||||
|
@ -26,28 +42,17 @@ class Orchestrator {
|
||||||
return step
|
return step
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(context) {
|
async execute() {
|
||||||
let workflow = this._workflow
|
let workflow = this._workflow
|
||||||
for (let block of workflow.definition.steps) {
|
for (let block of workflow.definition.steps) {
|
||||||
let step = await this.getStep(block.type, block.stepId)
|
let stepFn = await this.getStepFunctionality(block.type, block.stepId)
|
||||||
let args = { ...block.args }
|
block.inputs = recurseMustache(block.inputs, this._context)
|
||||||
// bind the workflow action args to the workflow context, if required
|
// instanceId is always passed
|
||||||
for (let arg of Object.keys(args)) {
|
const outputs = await stepFn({
|
||||||
const argValue = args[arg]
|
inputs: block.inputs,
|
||||||
// We don't want to render mustache templates on non-strings
|
instanceId: this._instanceId,
|
||||||
if (typeof argValue !== "string") continue
|
|
||||||
|
|
||||||
args[arg] = mustache.render(argValue, { context: this._context })
|
|
||||||
}
|
|
||||||
const response = await step({
|
|
||||||
args,
|
|
||||||
context,
|
|
||||||
})
|
})
|
||||||
|
this._context.blocks.push(outputs)
|
||||||
this._context = {
|
|
||||||
...this._context,
|
|
||||||
[block.id]: response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,8 +60,11 @@ class Orchestrator {
|
||||||
// callback is required for worker-farm to state that the worker thread has completed
|
// callback is required for worker-farm to state that the worker thread has completed
|
||||||
module.exports = async (job, cb = null) => {
|
module.exports = async (job, cb = null) => {
|
||||||
try {
|
try {
|
||||||
const workflowOrchestrator = new Orchestrator(job.data.workflow)
|
const workflowOrchestrator = new Orchestrator(
|
||||||
await workflowOrchestrator.execute(job.data.event)
|
job.data.workflow,
|
||||||
|
job.data.event
|
||||||
|
)
|
||||||
|
await workflowOrchestrator.execute()
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,71 @@ const InMemoryQueue = require("./queue/inMemoryQueue")
|
||||||
|
|
||||||
let workflowQueue = new InMemoryQueue()
|
let workflowQueue = new InMemoryQueue()
|
||||||
|
|
||||||
|
const BUILTIN_DEFINITIONS = {
|
||||||
|
RECORD_SAVED: {
|
||||||
|
name: "Record Saved",
|
||||||
|
event: "record:save",
|
||||||
|
icon: "ri-save-line",
|
||||||
|
tagline: "Record is added to {{inputs.enriched.model.name}}",
|
||||||
|
description: "Fired when a record is saved to your database",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
modelId: {
|
||||||
|
type: "string",
|
||||||
|
customType: "model",
|
||||||
|
title: "Table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["modelId"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
customType: "record",
|
||||||
|
description: "The new record that was saved",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["record"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "TRIGGER",
|
||||||
|
},
|
||||||
|
RECORD_DELETED: {
|
||||||
|
name: "Record Deleted",
|
||||||
|
event: "record:delete",
|
||||||
|
icon: "ri-delete-bin-line",
|
||||||
|
tagline: "Record is deleted from {{inputs.enriched.model.name}}",
|
||||||
|
description: "Fired when a record is deleted from your database",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
modelId: {
|
||||||
|
type: "string",
|
||||||
|
customType: "model",
|
||||||
|
title: "Table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["modelId"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
customType: "record",
|
||||||
|
description: "The record that was deleted",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["record"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: "TRIGGER",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
async function queueRelevantWorkflows(event, eventType) {
|
async function queueRelevantWorkflows(event, eventType) {
|
||||||
if (event.instanceId == null) {
|
if (event.instanceId == null) {
|
||||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||||
|
@ -36,3 +101,5 @@ module.exports.externalTrigger = async function(workflow, params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.workflowQueue = workflowQueue
|
module.exports.workflowQueue = workflowQueue
|
||||||
|
|
||||||
|
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||||
|
|
Loading…
Reference in New Issue