mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
42 changed files with 1534 additions and 578 deletions
@ -0,0 +1,58 @@ |
|||
<script> |
|||
import GenericBindingPopover from "./GenericBindingPopover.svelte" |
|||
import { Input, Icon } from "@budibase/bbui" |
|||
|
|||
export let bindings = [] |
|||
export let value |
|||
let anchor |
|||
let popover = undefined |
|||
let enrichedValue |
|||
let inputProps |
|||
|
|||
// Extract all other props to pass to input component |
|||
$: { |
|||
let { bindings, ...otherProps } = $$props |
|||
inputProps = otherProps |
|||
} |
|||
</script> |
|||
|
|||
<div class="container" bind:this={anchor}> |
|||
<Input {...inputProps} bind:value /> |
|||
<button on:click={popover.show}> |
|||
<Icon name="edit" /> |
|||
</button> |
|||
</div> |
|||
<GenericBindingPopover |
|||
{anchor} |
|||
{bindings} |
|||
bind:value |
|||
bind:popover |
|||
align="right" /> |
|||
|
|||
<style> |
|||
.container { |
|||
position: relative; |
|||
} |
|||
|
|||
button { |
|||
position: absolute; |
|||
background: none; |
|||
border: none; |
|||
border-radius: 50%; |
|||
height: 24px; |
|||
width: 24px; |
|||
background: var(--grey-4); |
|||
right: 7px; |
|||
bottom: 7px; |
|||
} |
|||
button:hover { |
|||
background: var(--grey-5); |
|||
cursor: pointer; |
|||
} |
|||
button :global(svg) { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translateX(-50%) translateY(-50%) !important; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,129 @@ |
|||
<script> |
|||
import groupBy from "lodash/fp/groupBy" |
|||
import { TextArea, Label, Body, Button, Popover } from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = "" |
|||
export let bindings = [] |
|||
export let anchor |
|||
export let align |
|||
export let popover = null |
|||
|
|||
$: categories = Object.entries(groupBy("category", bindings)) |
|||
|
|||
function onClickBinding(binding) { |
|||
value += `{{ ${binding.path} }}` |
|||
} |
|||
</script> |
|||
|
|||
<Popover {anchor} {align} bind:this={popover}> |
|||
<div class="container"> |
|||
<div class="bindings"> |
|||
<Label large>Available bindings</Label> |
|||
<div class="bindings__wrapper"> |
|||
<div class="bindings__list"> |
|||
{#each categories as [categoryName, bindings]} |
|||
<Label small>{categoryName}</Label> |
|||
{#each bindings as binding} |
|||
<div class="binding" on:click={() => onClickBinding(binding)}> |
|||
<span class="binding__label">{binding.label}</span> |
|||
<span class="binding__type">{binding.type}</span> |
|||
<br /> |
|||
<div class="binding__description">{binding.description}</div> |
|||
</div> |
|||
{/each} |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="editor"> |
|||
<Label large>Data binding</Label> |
|||
<Body small> |
|||
Binding connects one piece of data to another and makes it dynamic. |
|||
Click the objects on the left to add them to the textbox. |
|||
</Body> |
|||
<TextArea thin bind:value placeholder="..." /> |
|||
<div class="controls"> |
|||
<a href="#"> |
|||
<Body small>Learn more about binding</Body> |
|||
</a> |
|||
<Button on:click={popover.hide} primary>Done</Button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Popover> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-template-columns: 280px 1fr; |
|||
width: 800px; |
|||
} |
|||
|
|||
.bindings { |
|||
border-right: 1px solid var(--grey-4); |
|||
flex: 0 0 240px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
padding-right: var(--spacing-l); |
|||
} |
|||
.bindings :global(label) { |
|||
margin: var(--spacing-m) 0; |
|||
} |
|||
.bindings :global(label:first-child) { |
|||
margin-top: 0; |
|||
} |
|||
.bindings__wrapper { |
|||
overflow-y: auto; |
|||
position: relative; |
|||
flex: 1 1 auto; |
|||
} |
|||
.bindings__list { |
|||
position: absolute; |
|||
width: 100%; |
|||
} |
|||
|
|||
.binding { |
|||
font-size: 12px; |
|||
padding: var(--spacing-s); |
|||
border-radius: var(--border-radius-m); |
|||
} |
|||
.binding:hover { |
|||
background-color: var(--grey-2); |
|||
cursor: pointer; |
|||
} |
|||
.binding__label { |
|||
font-weight: 500; |
|||
text-transform: capitalize; |
|||
} |
|||
.binding__description { |
|||
color: var(--grey-8); |
|||
margin-top: 2px; |
|||
} |
|||
.binding__type { |
|||
font-family: monospace; |
|||
background-color: var(--grey-2); |
|||
border-radius: var(--border-radius-m); |
|||
padding: 2px; |
|||
margin-left: 2px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.editor { |
|||
padding-left: var(--spacing-l); |
|||
} |
|||
.editor :global(textarea) { |
|||
min-height: 60px; |
|||
} |
|||
|
|||
.controls { |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
margin-top: var(--spacing-m); |
|||
} |
|||
</style> |
|||
@ -1,23 +1,15 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { Select } from "@budibase/bbui" |
|||
|
|||
export let value |
|||
$: modelId = value ? value._id : "" |
|||
|
|||
function onChange(e) { |
|||
value = $backendUiStore.models.find(model => model._id === e.target.value) |
|||
} |
|||
</script> |
|||
|
|||
<div class="block-field"> |
|||
<select |
|||
class="budibase__input" |
|||
value={modelId} |
|||
on:blur={onChange} |
|||
on:change={onChange}> |
|||
<Select bind:value secondary thin> |
|||
<option value="">Choose an option</option> |
|||
{#each $backendUiStore.models as model} |
|||
<option value={model._id}>{model.name}</option> |
|||
{/each} |
|||
</select> |
|||
</Select> |
|||
</div> |
|||
|
|||
@ -1,51 +1,69 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { Input, Label } from "@budibase/bbui" |
|||
import { Input, Select, Label } from "@budibase/bbui" |
|||
import BindableInput from "../../../userInterface/BindableInput.svelte" |
|||
|
|||
export let value |
|||
$: modelId = value && value.model ? value.model._id : "" |
|||
$: schemaFields = Object.keys(value && value.model ? value.model.schema : {}) |
|||
export let bindings |
|||
|
|||
function onChangeModel(e) { |
|||
value.model = $backendUiStore.models.find( |
|||
model => model._id === e.target.value |
|||
) |
|||
} |
|||
$: model = $backendUiStore.models.find(model => model._id === value?.modelId) |
|||
$: schemaFields = Object.entries(model?.schema ?? {}) |
|||
|
|||
// Ensure any nullish modelId values get set to empty string so |
|||
// that the select works |
|||
$: if (value?.modelId == null) value = { modelId: "" } |
|||
|
|||
function setParsedValue(evt, field) { |
|||
const fieldSchema = value.model.schema[field] |
|||
if (fieldSchema.type === "number") { |
|||
value[field] = parseInt(evt.target.value) |
|||
return |
|||
} |
|||
value[field] = evt.target.value |
|||
function schemaHasOptions(schema) { |
|||
return !!schema.constraints?.inclusion?.length |
|||
} |
|||
</script> |
|||
|
|||
<div class="block-field"> |
|||
<select |
|||
class="budibase__input" |
|||
value={modelId} |
|||
on:blur={onChangeModel} |
|||
on:change={onChangeModel}> |
|||
<Select bind:value={value.modelId} thin secondary> |
|||
<option value="">Choose an option</option> |
|||
{#each $backendUiStore.models as model} |
|||
<option value={model._id}>{model.name}</option> |
|||
{/each} |
|||
</select> |
|||
</Select> |
|||
</div> |
|||
|
|||
{#if schemaFields.length} |
|||
<div class="bb-margin-xl block-field"> |
|||
<Label small forAttr={'fields'}>Fields</Label> |
|||
{#each schemaFields as field} |
|||
<div class="bb-margin-xl"> |
|||
<Input |
|||
thin |
|||
value={value[field]} |
|||
label={field} |
|||
on:change={e => setParsedValue(e, field)} /> |
|||
{#each schemaFields as [field, schema]} |
|||
<div class="bb-margin-xl capitalise"> |
|||
{#if schemaHasOptions(schema)} |
|||
<div class="field-label">{field}</div> |
|||
<Select thin secondary bind:value={value[field]}> |
|||
<option value="">Choose an option</option> |
|||
{#each schema.constraints.inclusion as option} |
|||
<option value={option}>{option}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if schema.type === 'string' || schema.type === 'number'} |
|||
<BindableInput |
|||
thin |
|||
bind:value={value[field]} |
|||
label={field} |
|||
type={schema.type} |
|||
{bindings} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
.field-label { |
|||
color: var(--ink); |
|||
margin-bottom: 12px; |
|||
display: flex; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
font-family: sans-serif; |
|||
} |
|||
|
|||
.capitalise :global(label), |
|||
.field-label { |
|||
text-transform: capitalize; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,77 @@ |
|||
<script> |
|||
import mustache from "mustache" |
|||
import { get } from "lodash/fp" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
export let block |
|||
|
|||
$: inputs = enrichInputs(block.inputs) |
|||
$: tagline = formatTagline(block.tagline, block.schema, inputs) |
|||
$: html = (tagline || "") |
|||
.replace(/{{\s*/g, "<span>") |
|||
.replace(/\s*}}/g, "</span>") |
|||
|
|||
function enrichInputs(inputs) { |
|||
let enrichedInputs = { ...inputs, enriched: {} } |
|||
const modelId = inputs.modelId || inputs.record?.modelId |
|||
if (modelId) { |
|||
enrichedInputs.enriched.model = $backendUiStore.models.find( |
|||
model => model._id === modelId |
|||
) |
|||
} |
|||
return enrichedInputs |
|||
} |
|||
|
|||
function formatTagline(tagline, schema, inputs) { |
|||
// Add bold tags around inputs |
|||
let formattedTagline = tagline |
|||
.replace(/{{/g, "<b>{{") |
|||
.replace(/}}/, "}}</b>") |
|||
|
|||
// Extract schema paths for any input bindings |
|||
const inputPaths = formattedTagline |
|||
.match(/{{\s*\S+\s*}}/g) |
|||
.map(x => x.replace(/[{}]/g, "").trim()) |
|||
const schemaPaths = inputPaths.map(x => x.replace(/\./g, ".properties.")) |
|||
|
|||
// Replace any enum bindings with their pretty equivalents |
|||
schemaPaths.forEach((path, idx) => { |
|||
const prettyValues = get(`${path}.pretty`, schema) |
|||
if (prettyValues) { |
|||
const enumValues = get(`${path}.enum`, schema) |
|||
const inputPath = inputPaths[idx] |
|||
const value = get(inputPath, { inputs }) |
|||
const valueIdx = enumValues.indexOf(value) |
|||
const prettyValue = prettyValues[valueIdx] |
|||
if (prettyValue == null) { |
|||
return |
|||
} |
|||
formattedTagline = formattedTagline.replace( |
|||
new RegExp(`{{\s*${inputPath}\s*}}`), |
|||
prettyValue |
|||
) |
|||
} |
|||
}) |
|||
|
|||
// Fill in bindings with mustache |
|||
return mustache.render(formattedTagline, { inputs }) |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
{@html html} |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
line-height: 1.25; |
|||
} |
|||
div :global(span) { |
|||
font-size: 0.9em; |
|||
background-color: var(--purple-light); |
|||
padding: var(--spacing-xs); |
|||
border-radius: var(--border-radius-m); |
|||
display: inline-block; |
|||
margin: 1px; |
|||
} |
|||
</style> |
|||
@ -1,113 +0,0 @@ |
|||
const ACTION = { |
|||
SAVE_RECORD: { |
|||
name: "Save Record", |
|||
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record", |
|||
icon: "ri-save-3-fill", |
|||
description: "Save a record to your database.", |
|||
params: { |
|||
record: "record", |
|||
}, |
|||
args: { |
|||
record: {}, |
|||
}, |
|||
type: "ACTION", |
|||
}, |
|||
DELETE_RECORD: { |
|||
description: "Delete a record from your database.", |
|||
icon: "ri-delete-bin-line", |
|||
name: "Delete Record", |
|||
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record", |
|||
params: {}, |
|||
args: {}, |
|||
type: "ACTION", |
|||
}, |
|||
CREATE_USER: { |
|||
description: "Create a new user.", |
|||
tagline: "Create user <b>{{username}}</b>", |
|||
icon: "ri-user-add-fill", |
|||
name: "Create User", |
|||
params: { |
|||
username: "string", |
|||
password: "password", |
|||
accessLevelId: "accessLevel", |
|||
}, |
|||
args: { |
|||
accessLevelId: "POWER_USER", |
|||
}, |
|||
type: "ACTION", |
|||
}, |
|||
SEND_EMAIL: { |
|||
description: "Send an email.", |
|||
tagline: "Send email to <b>{{to}}</b>", |
|||
icon: "ri-mail-open-fill", |
|||
name: "Send Email", |
|||
params: { |
|||
to: "string", |
|||
from: "string", |
|||
subject: "longText", |
|||
text: "longText", |
|||
}, |
|||
type: "ACTION", |
|||
}, |
|||
} |
|||
|
|||
const LOGIC = { |
|||
FILTER: { |
|||
name: "Filter", |
|||
tagline: "{{filter}} <b>{{condition}}</b> {{value}}", |
|||
icon: "ri-git-branch-line", |
|||
description: "Filter any workflows which do not meet certain conditions.", |
|||
params: { |
|||
filter: "string", |
|||
condition: ["equals"], |
|||
value: "string", |
|||
}, |
|||
args: { |
|||
condition: "equals", |
|||
}, |
|||
type: "LOGIC", |
|||
}, |
|||
DELAY: { |
|||
name: "Delay", |
|||
icon: "ri-time-fill", |
|||
tagline: "Delay for <b>{{time}}</b> milliseconds", |
|||
description: "Delay the workflow until an amount of time has passed.", |
|||
params: { |
|||
time: "number", |
|||
}, |
|||
type: "LOGIC", |
|||
}, |
|||
} |
|||
|
|||
const TRIGGER = { |
|||
RECORD_SAVED: { |
|||
name: "Record Saved", |
|||
event: "record:save", |
|||
icon: "ri-save-line", |
|||
tagline: "Record is added to <b>{{model.name}}</b>", |
|||
description: "Fired when a record is saved to your database.", |
|||
params: { |
|||
model: "model", |
|||
}, |
|||
type: "TRIGGER", |
|||
}, |
|||
RECORD_DELETED: { |
|||
name: "Record Deleted", |
|||
event: "record:delete", |
|||
icon: "ri-delete-bin-line", |
|||
tagline: "Record is deleted from <b>{{model.name}}</b>", |
|||
description: "Fired when a record is deleted from your database.", |
|||
params: { |
|||
model: "model", |
|||
}, |
|||
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, |
|||
} |
|||
@ -0,0 +1 @@ |
|||
module.exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms)) |
|||
@ -1,100 +1,36 @@ |
|||
const viewController = require("../api/controllers/view") |
|||
const modelController = require("../api/controllers/model") |
|||
const workflowController = require("../api/controllers/workflow") |
|||
|
|||
// Access Level IDs
|
|||
const ADMIN_LEVEL_ID = "ADMIN" |
|||
const POWERUSER_LEVEL_ID = "POWER_USER" |
|||
const BUILDER_LEVEL_ID = "BUILDER" |
|||
const ANON_LEVEL_ID = "ANON" |
|||
|
|||
// Permissions
|
|||
const READ_MODEL = "read-model" |
|||
const WRITE_MODEL = "write-model" |
|||
const READ_VIEW = "read-view" |
|||
const EXECUTE_WORKFLOW = "execute-workflow" |
|||
const USER_MANAGEMENT = "user-management" |
|||
const BUILDER = "builder" |
|||
const LIST_USERS = "list-users" |
|||
|
|||
const adminPermissions = [ |
|||
module.exports.READ_MODEL = "read-model" |
|||
module.exports.WRITE_MODEL = "write-model" |
|||
module.exports.READ_VIEW = "read-view" |
|||
module.exports.EXECUTE_WORKFLOW = "execute-workflow" |
|||
module.exports.USER_MANAGEMENT = "user-management" |
|||
module.exports.BUILDER = "builder" |
|||
module.exports.LIST_USERS = "list-users" |
|||
// Access Level IDs
|
|||
module.exports.ADMIN_LEVEL_ID = "ADMIN" |
|||
module.exports.POWERUSER_LEVEL_ID = "POWER_USER" |
|||
module.exports.BUILDER_LEVEL_ID = "BUILDER" |
|||
module.exports.ANON_LEVEL_ID = "ANON" |
|||
module.exports.ACCESS_LEVELS = [ |
|||
module.exports.ADMIN_LEVEL_ID, |
|||
module.exports.POWERUSER_LEVEL_ID, |
|||
module.exports.BUILDER_LEVEL_ID, |
|||
module.exports.ANON_LEVEL_ID, |
|||
] |
|||
module.exports.PRETTY_ACCESS_LEVELS = { |
|||
[module.exports.ADMIN_LEVEL_ID]: "Admin", |
|||
[module.exports.POWERUSER_LEVEL_ID]: "Power user", |
|||
[module.exports.BUILDER_LEVEL_ID]: "Builder", |
|||
[module.exports.ANON_LEVEL_ID]: "Anonymous", |
|||
} |
|||
module.exports.adminPermissions = [ |
|||
{ |
|||
name: USER_MANAGEMENT, |
|||
name: module.exports.USER_MANAGEMENT, |
|||
}, |
|||
] |
|||
|
|||
const generateAdminPermissions = async instanceId => [ |
|||
...adminPermissions, |
|||
...(await generatePowerUserPermissions(instanceId)), |
|||
] |
|||
|
|||
const generatePowerUserPermissions = async instanceId => { |
|||
const fetchModelsCtx = { |
|||
user: { |
|||
instanceId, |
|||
}, |
|||
} |
|||
await modelController.fetch(fetchModelsCtx) |
|||
const models = fetchModelsCtx.body |
|||
|
|||
const fetchViewsCtx = { |
|||
user: { |
|||
instanceId, |
|||
}, |
|||
} |
|||
await viewController.fetch(fetchViewsCtx) |
|||
const views = fetchViewsCtx.body |
|||
|
|||
const fetchWorkflowsCtx = { |
|||
user: { |
|||
instanceId, |
|||
}, |
|||
} |
|||
await workflowController.fetch(fetchWorkflowsCtx) |
|||
const workflows = fetchWorkflowsCtx.body |
|||
|
|||
const readModelPermissions = models.map(m => ({ |
|||
itemId: m._id, |
|||
name: READ_MODEL, |
|||
})) |
|||
|
|||
const writeModelPermissions = models.map(m => ({ |
|||
itemId: m._id, |
|||
name: WRITE_MODEL, |
|||
})) |
|||
|
|||
const viewPermissions = views.map(v => ({ |
|||
itemId: v.name, |
|||
name: READ_VIEW, |
|||
})) |
|||
|
|||
const executeWorkflowPermissions = workflows.map(w => ({ |
|||
itemId: w._id, |
|||
name: EXECUTE_WORKFLOW, |
|||
})) |
|||
|
|||
return [ |
|||
...readModelPermissions, |
|||
...writeModelPermissions, |
|||
...viewPermissions, |
|||
...executeWorkflowPermissions, |
|||
{ name: LIST_USERS }, |
|||
] |
|||
} |
|||
|
|||
module.exports = { |
|||
ADMIN_LEVEL_ID, |
|||
POWERUSER_LEVEL_ID, |
|||
BUILDER_LEVEL_ID, |
|||
ANON_LEVEL_ID, |
|||
READ_MODEL, |
|||
WRITE_MODEL, |
|||
READ_VIEW, |
|||
EXECUTE_WORKFLOW, |
|||
USER_MANAGEMENT, |
|||
BUILDER, |
|||
LIST_USERS, |
|||
adminPermissions, |
|||
generateAdminPermissions, |
|||
generatePowerUserPermissions, |
|||
} |
|||
// to avoid circular dependencies this is included later, after exporting all enums
|
|||
const permissions = require("./permissions") |
|||
module.exports.generateAdminPermissions = permissions.generateAdminPermissions |
|||
module.exports.generatePowerUserPermissions = |
|||
permissions.generatePowerUserPermissions |
|||
|
|||
@ -0,0 +1,66 @@ |
|||
const viewController = require("../api/controllers/view") |
|||
const modelController = require("../api/controllers/model") |
|||
const workflowController = require("../api/controllers/workflow") |
|||
const accessLevels = require("./accessLevels") |
|||
|
|||
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
|
|||
const generateAdminPermissions = async instanceId => [ |
|||
...accessLevels.adminPermissions, |
|||
...(await generatePowerUserPermissions(instanceId)), |
|||
] |
|||
|
|||
const generatePowerUserPermissions = async instanceId => { |
|||
const fetchModelsCtx = { |
|||
user: { |
|||
instanceId, |
|||
}, |
|||
} |
|||
await modelController.fetch(fetchModelsCtx) |
|||
const models = fetchModelsCtx.body |
|||
|
|||
const fetchViewsCtx = { |
|||
user: { |
|||
instanceId, |
|||
}, |
|||
} |
|||
await viewController.fetch(fetchViewsCtx) |
|||
const views = fetchViewsCtx.body |
|||
|
|||
const fetchWorkflowsCtx = { |
|||
user: { |
|||
instanceId, |
|||
}, |
|||
} |
|||
await workflowController.fetch(fetchWorkflowsCtx) |
|||
const workflows = fetchWorkflowsCtx.body |
|||
|
|||
const readModelPermissions = models.map(m => ({ |
|||
itemId: m._id, |
|||
name: accessLevels.READ_MODEL, |
|||
})) |
|||
|
|||
const writeModelPermissions = models.map(m => ({ |
|||
itemId: m._id, |
|||
name: accessLevels.WRITE_MODEL, |
|||
})) |
|||
|
|||
const viewPermissions = views.map(v => ({ |
|||
itemId: v.name, |
|||
name: accessLevels.READ_VIEW, |
|||
})) |
|||
|
|||
const executeWorkflowPermissions = workflows.map(w => ({ |
|||
itemId: w._id, |
|||
name: accessLevels.EXECUTE_WORKFLOW, |
|||
})) |
|||
|
|||
return [ |
|||
...readModelPermissions, |
|||
...writeModelPermissions, |
|||
...viewPermissions, |
|||
...executeWorkflowPermissions, |
|||
{ name: accessLevels.LIST_USERS }, |
|||
] |
|||
} |
|||
module.exports.generateAdminPermissions = generateAdminPermissions |
|||
module.exports.generatePowerUserPermissions = generatePowerUserPermissions |
|||
@ -1,24 +1,20 @@ |
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) |
|||
let filter = require("./steps/filter") |
|||
let delay = require("./steps/delay") |
|||
|
|||
let LOGIC = { |
|||
DELAY: async function delay({ args }) { |
|||
await wait(args.time) |
|||
}, |
|||
let BUILTIN_LOGIC = { |
|||
DELAY: delay.run, |
|||
FILTER: filter.run, |
|||
} |
|||
|
|||
FILTER: async function filter({ args }) { |
|||
const { field, condition, value } = args |
|||
switch (condition) { |
|||
case "equals": |
|||
if (field !== value) return |
|||
break |
|||
default: |
|||
return |
|||
} |
|||
}, |
|||
let BUILTIN_DEFINITIONS = { |
|||
DELAY: delay.definition, |
|||
FILTER: filter.definition, |
|||
} |
|||
|
|||
module.exports.getLogic = function(logicName) { |
|||
if (LOGIC[logicName] != null) { |
|||
return LOGIC[logicName] |
|||
if (BUILTIN_LOGIC[logicName] != null) { |
|||
return BUILTIN_LOGIC[logicName] |
|||
} |
|||
} |
|||
|
|||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS |
|||
|
|||
@ -0,0 +1,86 @@ |
|||
const accessLevels = require("../../utilities/accessLevels") |
|||
const userController = require("../../api/controllers/user") |
|||
|
|||
module.exports.definition = { |
|||
description: "Create a new user", |
|||
tagline: "Create user {{inputs.username}}", |
|||
icon: "ri-user-add-fill", |
|||
name: "Create User", |
|||
type: "ACTION", |
|||
stepId: "CREATE_USER", |
|||
inputs: { |
|||
accessLevelId: accessLevels.POWERUSER_LEVEL_ID, |
|||
}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
username: { |
|||
type: "string", |
|||
title: "Username", |
|||
}, |
|||
password: { |
|||
type: "string", |
|||
customType: "password", |
|||
title: "Password", |
|||
}, |
|||
accessLevelId: { |
|||
type: "string", |
|||
title: "Access Level", |
|||
enum: accessLevels.ACCESS_LEVELS, |
|||
pretty: Object.values(accessLevels.PRETTY_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, |
|||
// internal property not returned through the API
|
|||
id: ctx.userId, |
|||
revision: ctx.body._rev, |
|||
success: ctx.status === 200, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
response: err, |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
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", |
|||
stepId: "DELAY", |
|||
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,79 @@ |
|||
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", |
|||
stepId: "DELETE_RECORD", |
|||
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, |
|||
record: ctx.record, |
|||
success: ctx.status === 200, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
response: err, |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
const LogicConditions = { |
|||
EQUAL: "EQUAL", |
|||
NOT_EQUAL: "NOT_EQUAL", |
|||
GREATER_THAN: "GREATER_THAN", |
|||
LESS_THAN: "LESS_THAN", |
|||
} |
|||
|
|||
const PrettyLogicConditions = { |
|||
[LogicConditions.EQUAL]: "Equals", |
|||
[LogicConditions.NOT_EQUAL]: "Not equals", |
|||
[LogicConditions.GREATER_THAN]: "Greater than", |
|||
[LogicConditions.LESS_THAN]: "Less than", |
|||
} |
|||
|
|||
module.exports.definition = { |
|||
name: "Filter", |
|||
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}", |
|||
icon: "ri-git-branch-line", |
|||
description: "Filter any workflows which do not meet certain conditions", |
|||
type: "LOGIC", |
|||
stepId: "FILTER", |
|||
inputs: { |
|||
condition: LogicConditions.EQUALS, |
|||
}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
field: { |
|||
type: "string", |
|||
title: "Reference Value", |
|||
}, |
|||
condition: { |
|||
type: "string", |
|||
title: "Condition", |
|||
enum: Object.values(LogicConditions), |
|||
pretty: Object.values(PrettyLogicConditions), |
|||
}, |
|||
value: { |
|||
type: "string", |
|||
title: "Comparison Value", |
|||
}, |
|||
}, |
|||
required: ["field", "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.EQUAL: |
|||
success = field === value |
|||
break |
|||
case LogicConditions.NOT_EQUAL: |
|||
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,90 @@ |
|||
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", |
|||
stepId: "SAVE_RECORD", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
record: { |
|||
type: "object", |
|||
properties: { |
|||
modelId: { |
|||
type: "string", |
|||
customType: "model", |
|||
}, |
|||
}, |
|||
customType: "record", |
|||
title: "Table", |
|||
required: ["modelId"], |
|||
}, |
|||
}, |
|||
required: ["record"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
record: { |
|||
type: "object", |
|||
customType: "record", |
|||
description: "The new 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 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 { |
|||
record: inputs.record, |
|||
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,72 @@ |
|||
const environment = require("../../environment") |
|||
const sgMail = require("@sendgrid/mail") |
|||
sgMail.setApiKey(environment.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", |
|||
stepId: "SEND_EMAIL", |
|||
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.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, |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue