mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
30 changed files with 956 additions and 519 deletions
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 303 B |
@ -1,27 +1,91 @@ |
|||||
const sendEmail = require("./steps/sendEmail") |
const sendEmail = require("./steps/sendEmail") |
||||
const saveRecord = require("./steps/saveRecord") |
const saveRecord = require("./steps/saveRecord") |
||||
|
const updateRecord = require("./steps/updateRecord") |
||||
const deleteRecord = require("./steps/deleteRecord") |
const deleteRecord = require("./steps/deleteRecord") |
||||
const createUser = require("./steps/createUser") |
const createUser = require("./steps/createUser") |
||||
|
const environment = require("../environment") |
||||
|
const download = require("download") |
||||
|
const fetch = require("node-fetch") |
||||
|
const path = require("path") |
||||
|
const os = require("os") |
||||
|
const fs = require("fs") |
||||
|
const Sentry = require("@sentry/node") |
||||
|
|
||||
|
const DEFAULT_BUCKET = |
||||
|
"https://prod-budi-automations.s3-eu-west-1.amazonaws.com" |
||||
|
const DEFAULT_DIRECTORY = ".budibase-automations" |
||||
|
const AUTOMATION_MANIFEST = "manifest.json" |
||||
const BUILTIN_ACTIONS = { |
const BUILTIN_ACTIONS = { |
||||
SEND_EMAIL: sendEmail.run, |
SEND_EMAIL: sendEmail.run, |
||||
SAVE_RECORD: saveRecord.run, |
SAVE_RECORD: saveRecord.run, |
||||
|
UPDATE_RECORD: updateRecord.run, |
||||
DELETE_RECORD: deleteRecord.run, |
DELETE_RECORD: deleteRecord.run, |
||||
CREATE_USER: createUser.run, |
CREATE_USER: createUser.run, |
||||
} |
} |
||||
|
|
||||
const BUILTIN_DEFINITIONS = { |
const BUILTIN_DEFINITIONS = { |
||||
SEND_EMAIL: sendEmail.definition, |
SEND_EMAIL: sendEmail.definition, |
||||
SAVE_RECORD: saveRecord.definition, |
SAVE_RECORD: saveRecord.definition, |
||||
|
UPDATE_RECORD: updateRecord.definition, |
||||
DELETE_RECORD: deleteRecord.definition, |
DELETE_RECORD: deleteRecord.definition, |
||||
CREATE_USER: createUser.definition, |
CREATE_USER: createUser.definition, |
||||
} |
} |
||||
|
|
||||
|
let AUTOMATION_BUCKET = environment.AUTOMATION_BUCKET |
||||
|
let AUTOMATION_DIRECTORY = environment.AUTOMATION_DIRECTORY |
||||
|
let MANIFEST = null |
||||
|
|
||||
|
function buildBundleName(pkgName, version) { |
||||
|
return `${pkgName}@${version}.min.js` |
||||
|
} |
||||
|
|
||||
|
async function downloadPackage(name, version, bundleName) { |
||||
|
await download( |
||||
|
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`, |
||||
|
AUTOMATION_DIRECTORY |
||||
|
) |
||||
|
return require(path.join(AUTOMATION_DIRECTORY, bundleName)) |
||||
|
} |
||||
|
|
||||
module.exports.getAction = async function(actionName) { |
module.exports.getAction = async function(actionName) { |
||||
if (BUILTIN_ACTIONS[actionName] != null) { |
if (BUILTIN_ACTIONS[actionName] != null) { |
||||
return BUILTIN_ACTIONS[actionName] |
return BUILTIN_ACTIONS[actionName] |
||||
} |
} |
||||
// TODO: load async actions here
|
// env setup to get async packages
|
||||
|
if (!MANIFEST || !MANIFEST.packages || !MANIFEST.packages[actionName]) { |
||||
|
return null |
||||
|
} |
||||
|
const pkg = MANIFEST.packages[actionName] |
||||
|
const bundleName = buildBundleName(pkg.stepId, pkg.version) |
||||
|
try { |
||||
|
return require(path.join(AUTOMATION_DIRECTORY, bundleName)) |
||||
|
} catch (err) { |
||||
|
return downloadPackage(pkg.stepId, pkg.version, bundleName) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports.init = async function() { |
||||
|
// set defaults
|
||||
|
if (!AUTOMATION_DIRECTORY) { |
||||
|
AUTOMATION_DIRECTORY = path.join(os.homedir(), DEFAULT_DIRECTORY) |
||||
|
} |
||||
|
if (!AUTOMATION_BUCKET) { |
||||
|
AUTOMATION_BUCKET = DEFAULT_BUCKET |
||||
|
} |
||||
|
if (!fs.existsSync(AUTOMATION_DIRECTORY)) { |
||||
|
fs.mkdirSync(AUTOMATION_DIRECTORY, { recursive: true }) |
||||
|
} |
||||
|
// env setup to get async packages
|
||||
|
try { |
||||
|
let response = await fetch(`${AUTOMATION_BUCKET}/${AUTOMATION_MANIFEST}`) |
||||
|
MANIFEST = await response.json() |
||||
|
module.exports.DEFINITIONS = |
||||
|
MANIFEST && MANIFEST.packages |
||||
|
? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS) |
||||
|
: BUILTIN_DEFINITIONS |
||||
|
} catch (err) { |
||||
|
Sentry.captureException(err) |
||||
|
} |
||||
} |
} |
||||
|
|
||||
|
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS |
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS |
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS |
||||
|
|||||
@ -0,0 +1,116 @@ |
|||||
|
const CouchDB = require("../db") |
||||
|
|
||||
|
/** |
||||
|
* When running mustache statements to execute on the context of the automation it possible user's may input mustache |
||||
|
* in a few different forms, some of which are invalid but are logically valid. An example of this would be the mustache |
||||
|
* statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array |
||||
|
* like operators. These are not supported by Mustache and therefore the statement will fail. This function will clean up |
||||
|
* the mustache statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded |
||||
|
* to include any other mustache statement cleanup that has been deemed necessary for the system. |
||||
|
* |
||||
|
* @param {string} string The string which *may* contain mustache statements, it is OK if it does not contain any. |
||||
|
* @returns {string} The string that was input with cleaned up mustache statements as required. |
||||
|
*/ |
||||
|
module.exports.cleanMustache = string => { |
||||
|
let charToReplace = { |
||||
|
"[": ".", |
||||
|
"]": "", |
||||
|
} |
||||
|
let regex = new RegExp(/{{[^}}]*}}/g) |
||||
|
let matches = string.match(regex) |
||||
|
if (matches == null) { |
||||
|
return string |
||||
|
} |
||||
|
for (let match of matches) { |
||||
|
let baseIdx = string.indexOf(match) |
||||
|
for (let key of Object.keys(charToReplace)) { |
||||
|
let idxChar = match.indexOf(key) |
||||
|
if (idxChar !== -1) { |
||||
|
string = |
||||
|
string.slice(baseIdx, baseIdx + idxChar) + |
||||
|
charToReplace[key] + |
||||
|
string.slice(baseIdx + idxChar + 1) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return string |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* When values are input to the system generally they will be of type string as this is required for mustache. This can |
||||
|
* generate some odd scenarios as the Schema of the automation requires a number but the builder might supply a string |
||||
|
* with mustache syntax to get the number from the rest of the context. To support this the server has to make sure that |
||||
|
* the post mustache statement can be cast into the correct type, this function does this for numbers and booleans. |
||||
|
* |
||||
|
* @param {object} inputs An object of inputs, please note this will not recurse down into any objects within, it simply |
||||
|
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if |
||||
|
* the schema is known. |
||||
|
* @param {object} schema The defined schema of the inputs, in the form of JSON schema. The schema definition of an |
||||
|
* automation is the likely use case of this, however validate.js syntax can be converted closely enough to use this by |
||||
|
* wrapping the schema properties in a top level "properties" object. |
||||
|
* @returns {object} The inputs object which has had all the various types supported by this function converted to their |
||||
|
* primitive types. |
||||
|
*/ |
||||
|
module.exports.cleanInputValues = (inputs, schema) => { |
||||
|
if (schema == null) { |
||||
|
return inputs |
||||
|
} |
||||
|
for (let inputKey of Object.keys(inputs)) { |
||||
|
let input = inputs[inputKey] |
||||
|
if (typeof input !== "string") { |
||||
|
continue |
||||
|
} |
||||
|
let propSchema = schema.properties[inputKey] |
||||
|
if (!propSchema) { |
||||
|
continue |
||||
|
} |
||||
|
if (propSchema.type === "boolean") { |
||||
|
let lcInput = input.toLowerCase() |
||||
|
if (lcInput === "true") { |
||||
|
inputs[inputKey] = true |
||||
|
} |
||||
|
if (lcInput === "false") { |
||||
|
inputs[inputKey] = false |
||||
|
} |
||||
|
} |
||||
|
if (propSchema.type === "number") { |
||||
|
let floatInput = parseFloat(input) |
||||
|
if (!isNaN(floatInput)) { |
||||
|
inputs[inputKey] = floatInput |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return inputs |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Given a record input like a save or update record we need to clean the inputs against a schema that is not part of |
||||
|
* the automation but is instead part of the Table/Model. This function will get the model schema and use it to instead |
||||
|
* perform the cleanInputValues function on the input record. |
||||
|
* |
||||
|
* @param {string} instanceId The instance which the Table/Model is contained under. |
||||
|
* @param {string} modelId The ID of the Table/Model which the schema is to be retrieved for. |
||||
|
* @param {object} record The input record structure which requires clean-up after having been through mustache statements. |
||||
|
* @returns {Promise<Object>} The cleaned up records object, will should now have all the required primitive types. |
||||
|
*/ |
||||
|
module.exports.cleanUpRecord = async (instanceId, modelId, record) => { |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const model = await db.get(modelId) |
||||
|
|
||||
|
return module.exports.cleanInputValues(record, { properties: model.schema }) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* A utility function for the cleanUpRecord, which can be used if only the record ID is known (not the model ID) to clean |
||||
|
* up a record after mustache statements have been replaced. This is specifically useful for the update record action. |
||||
|
* |
||||
|
* @param {string} instanceId The instance which the Table/Model is contained under. |
||||
|
* @param {string} recordId The ID of the record from which the modelId will be extracted, to get the Table/Model schema. |
||||
|
* @param {object} record The input record structure which requires clean-up after having been through mustache statements. |
||||
|
* @returns {Promise<Object>} The cleaned up records object, which will now have all the required primitive types. |
||||
|
*/ |
||||
|
module.exports.cleanUpRecordById = async (instanceId, recordId, record) => { |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const foundRecord = await db.get(recordId) |
||||
|
return module.exports.cleanUpRecord(instanceId, foundRecord.modelId, record) |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
const recordController = require("../../api/controllers/record") |
||||
|
const automationUtils = require("../automationUtils") |
||||
|
|
||||
|
module.exports.definition = { |
||||
|
name: "Update Record", |
||||
|
tagline: "Update a {{inputs.enriched.model.name}} record", |
||||
|
icon: "ri-refresh-fill", |
||||
|
description: "Update a record to your database", |
||||
|
type: "ACTION", |
||||
|
stepId: "UPDATE_RECORD", |
||||
|
inputs: {}, |
||||
|
schema: { |
||||
|
inputs: { |
||||
|
properties: { |
||||
|
record: { |
||||
|
type: "object", |
||||
|
customType: "record", |
||||
|
title: "Record", |
||||
|
}, |
||||
|
recordId: { |
||||
|
type: "string", |
||||
|
title: "Record ID", |
||||
|
}, |
||||
|
}, |
||||
|
required: ["record", "recordId"], |
||||
|
}, |
||||
|
outputs: { |
||||
|
properties: { |
||||
|
record: { |
||||
|
type: "object", |
||||
|
customType: "record", |
||||
|
description: "The updated record", |
||||
|
}, |
||||
|
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 updated record", |
||||
|
}, |
||||
|
revision: { |
||||
|
type: "string", |
||||
|
description: "The revision of the updated record", |
||||
|
}, |
||||
|
}, |
||||
|
required: ["success", "id", "revision"], |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
module.exports.run = async function({ inputs, instanceId }) { |
||||
|
if (inputs.recordId == null || inputs.record == null) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
inputs.record = await automationUtils.cleanUpRecordById( |
||||
|
instanceId, |
||||
|
inputs.recordId, |
||||
|
inputs.record |
||||
|
) |
||||
|
// clear any falsy properties so that they aren't updated
|
||||
|
for (let propKey of Object.keys(inputs.record)) { |
||||
|
if (!inputs.record[propKey] || inputs.record[propKey] === "") { |
||||
|
delete inputs.record[propKey] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// have to clean up the record, remove the model from it
|
||||
|
const ctx = { |
||||
|
params: { |
||||
|
id: inputs.recordId, |
||||
|
}, |
||||
|
request: { |
||||
|
body: inputs.record, |
||||
|
}, |
||||
|
user: { instanceId }, |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await recordController.patch(ctx) |
||||
|
return { |
||||
|
record: ctx.body, |
||||
|
response: ctx.message, |
||||
|
id: ctx.body._id, |
||||
|
revision: ctx.body._rev, |
||||
|
success: ctx.status === 200, |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error(err) |
||||
|
return { |
||||
|
success: false, |
||||
|
response: err, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue