diff --git a/lerna.json b/lerna.json index 8970275c9c..1de84eca6d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index 23d8f899d4..afc0e99301 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js index e0936182a7..669192a905 100644 --- a/packages/auth/src/cache/user.js +++ b/packages/auth/src/cache/user.js @@ -3,7 +3,27 @@ const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy") const EXPIRY_SECONDS = 3600 -exports.getUser = async (userId, tenantId = null) => { +/** + * The default populate user function + */ +const populateFromDB = (userId, tenantId) => { + return getGlobalDB(tenantId).get(userId) +} + +/** + * Get the requested user by id. + * Use redis cache to first read the user. + * If not present fallback to loading the user directly and re-caching. + * @param {*} userId the id of the user to get + * @param {*} tenantId the tenant of the user to get + * @param {*} populateUser function to provide the user for re-caching. default to couch db + * @returns + */ +exports.getUser = async ( + userId, + tenantId = null, + populateUser = populateFromDB +) => { if (!tenantId) { try { tenantId = getTenantId() @@ -15,7 +35,7 @@ exports.getUser = async (userId, tenantId = null) => { // try cache let user = await client.get(userId) if (!user) { - user = await getGlobalDB(tenantId).get(userId) + user = await populateUser(userId, tenantId) client.store(userId, user, EXPIRY_SECONDS) } if (user && !user.tenantId && tenantId) { diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index 99aef0287d..a5d7c1f100 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -35,10 +35,6 @@ exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR exports.APP_DEV = exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR exports.SEPARATOR = SEPARATOR -function isDevApp(app) { - return app.appId.startsWith(exports.APP_DEV_PREFIX) -} - /** * If creating DB allDocs/query params with only a single top level ID this can be used, this * is usually the case as most of our docs are top level e.g. tables, automations, users and so on. @@ -62,6 +58,18 @@ function getDocParams(docType, docId = null, otherProps = {}) { } } +exports.isDevAppID = appId => { + return appId.startsWith(exports.APP_DEV_PREFIX) +} + +exports.isProdAppID = appId => { + return appId.startsWith(exports.APP_PREFIX) && !exports.isDevAppID(appId) +} + +function isDevApp(app) { + return exports.isDevAppID(app.appId) +} + /** * Given an app ID this will attempt to retrieve the tenant ID from it. * @return {null|string} The tenant ID found within the app ID. diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index e3705a9a24..944f3ee9d9 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -21,7 +21,10 @@ function finalise( * The tenancy modules should not be used here and it should be assumed that the tenancy context * has not yet been populated. */ -module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => { +module.exports = ( + noAuthPatterns = [], + opts = { publicAllowed: false, populateUser: null } +) => { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] return async (ctx, next) => { let publicEndpoint = false @@ -46,7 +49,15 @@ module.exports = (noAuthPatterns = [], opts = { publicAllowed: false }) => { error = "No session found" } else { try { - user = await getUser(userId, session.tenantId) + if (opts && opts.populateUser) { + user = await getUser( + userId, + session.tenantId, + opts.populateUser(ctx) + ) + } else { + user = await getUser(userId, session.tenantId) + } delete user.password authenticated = true } catch (err) { diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index 09b4905298..6befecd9ba 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -8,11 +8,13 @@ const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD exports.Databases = { PW_RESETS: "pwReset", + VERIFICATIONS: "verification", INVITATIONS: "invitation", DEV_LOCKS: "devLocks", DEBOUNCE: "debounce", SESSIONS: "session", USER_CACHE: "users", + FLAGS: "flags", } exports.SEPARATOR = SEPARATOR diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 91a16ae9c5..a060614934 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index e82eeff670..e5040c3c45 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -24,7 +24,7 @@ context("Create a automation", () => { }) // Create action - cy.contains("Action").click() + cy.contains("Internal").click() cy.contains("Create Row").click() cy.get(".setup").within(() => { cy.get(".spectrum-Picker-label").click() diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js index 4ad8e5287d..1a6f1d5b2b 100644 --- a/packages/builder/cypress/setup.js +++ b/packages/builder/cypress/setup.js @@ -23,6 +23,7 @@ process.env.MINIO_SECRET_KEY = "budibase" process.env.COUCH_DB_USER = "budibase" process.env.COUCH_DB_PASSWORD = "budibase" process.env.INTERNAL_API_KEY = "budibase" +process.env.ALLOW_DEV_AUTOMATIONS = 1 // Stop info logs polluting test outputs process.env.LOG_LEVEL = "error" diff --git a/packages/builder/package.json b/packages/builder/package.json index 0322ebeb6b..66c1acdf67 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.125-alpha.2", - "@budibase/client": "^0.9.125-alpha.2", + "@budibase/bbui": "^0.9.125-alpha.5", + "@budibase/client": "^0.9.125-alpha.5", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.125-alpha.2", + "@budibase/string-templates": "^0.9.125-alpha.5", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index c372f27bb7..f8e7db04a0 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -78,8 +78,11 @@ const automationActions = store => ({ }, trigger: async ({ automation }) => { const { _id } = automation - const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger` - return await api.post(TRIGGER_AUTOMATION_URL) + return await api.post(`/api/automations/${_id}/trigger`) + }, + test: async ({ automation }) => { + const { _id } = automation + return await api.post(`/api/automations/${_id}/test`) }, select: automation => { store.update(state => { diff --git a/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte b/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte index af5c9e449e..a04541cfad 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/BlockList.svelte @@ -14,15 +14,17 @@ disabled: hasTrigger, }, { - label: "Action", + label: "Internal", value: "ACTION", + internal: true, icon: "Actions", disabled: !hasTrigger, }, { - label: "Logic", - value: "LOGIC", - icon: "Filter", + label: "External", + value: "ACTION", + internal: false, + icon: "Extension", disabled: !hasTrigger, }, ] @@ -32,9 +34,13 @@ let popover let webhookModal $: selectedTab = selectedIndex == null ? null : tabs[selectedIndex].value + $: selectedInternal = + selectedIndex == null ? null : tabs[selectedIndex].internal $: anchor = selectedIndex === -1 ? null : anchors[selectedIndex] $: blocks = sortBy(entry => entry[1].name)( Object.entries($automationStore.blockDefinitions[selectedTab] ?? {}) + ).filter( + entry => selectedInternal == null || entry[1].internal === selectedInternal ) function onChangeTab(idx) { diff --git a/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte b/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte index 3ba59f36a6..bf6c45074c 100644 --- a/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte +++ b/packages/builder/src/components/automation/SetupPanel/SetupPanel.svelte @@ -25,7 +25,7 @@ } async function testAutomation() { - const result = await automationStore.actions.trigger({ + const result = await automationStore.actions.test({ automation: $automationStore.selectedAutomation.automation, }) if (result.status === 200) { diff --git a/packages/cli/package.json b/packages/cli/package.json index fba3d90e6f..d8ef49de10 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index 18a43bad22..a15b39b591 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^0.9.125-alpha.2", + "@budibase/bbui": "^0.9.125-alpha.5", "@budibase/standard-components": "^0.9.124", - "@budibase/string-templates": "^0.9.125-alpha.2", + "@budibase/string-templates": "^0.9.125-alpha.5", "regexparam": "^1.3.0", "shortid": "^2.2.15", "svelte-spa-router": "^3.0.5" diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock index 29e6b17613..5e6e414769 100644 --- a/packages/client/yarn.lock +++ b/packages/client/yarn.lock @@ -77,6 +77,83 @@ svelte-flatpickr "^3.1.0" svelte-portal "^1.0.0" +"@budibase/bbui@^0.9.125-alpha.5": + version "0.9.128" + resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.128.tgz#71a5e81b02a4f64baa6874bfa4b1d4fed8cc9f0b" + integrity sha512-UCo9SRs1xsMQ0ClJOtNB7JeVF2+0iMVOfAZE7Rrf5d51W+1YeyM9b3BbfhxHRq1yeKLHrbdczerbTHQkmF1RwQ== + dependencies: + "@adobe/spectrum-css-workflow-icons" "^1.2.1" + "@spectrum-css/actionbutton" "^1.0.1" + "@spectrum-css/actiongroup" "^1.0.1" + "@spectrum-css/avatar" "^3.0.2" + "@spectrum-css/button" "^3.0.1" + "@spectrum-css/buttongroup" "^3.0.2" + "@spectrum-css/checkbox" "^3.0.2" + "@spectrum-css/dialog" "^3.0.1" + "@spectrum-css/divider" "^1.0.3" + "@spectrum-css/dropzone" "^3.0.2" + "@spectrum-css/fieldgroup" "^3.0.2" + "@spectrum-css/fieldlabel" "^3.0.1" + "@spectrum-css/icon" "^3.0.1" + "@spectrum-css/illustratedmessage" "^3.0.2" + "@spectrum-css/inputgroup" "^3.0.2" + "@spectrum-css/label" "^2.0.10" + "@spectrum-css/link" "^3.1.1" + "@spectrum-css/menu" "^3.0.1" + "@spectrum-css/modal" "^3.0.1" + "@spectrum-css/pagination" "^3.0.3" + "@spectrum-css/picker" "^1.0.1" + "@spectrum-css/popover" "^3.0.1" + "@spectrum-css/progressbar" "^1.0.2" + "@spectrum-css/progresscircle" "^1.0.2" + "@spectrum-css/radio" "^3.0.2" + "@spectrum-css/search" "^3.0.2" + "@spectrum-css/sidenav" "^3.0.2" + "@spectrum-css/statuslight" "^3.0.2" + "@spectrum-css/stepper" "^3.0.3" + "@spectrum-css/switch" "^1.0.2" + "@spectrum-css/table" "^3.0.1" + "@spectrum-css/tabs" "^3.0.1" + "@spectrum-css/tags" "^3.0.2" + "@spectrum-css/textfield" "^3.0.1" + "@spectrum-css/toast" "^3.0.1" + "@spectrum-css/tooltip" "^3.0.3" + "@spectrum-css/treeview" "^3.0.2" + "@spectrum-css/typography" "^3.0.1" + "@spectrum-css/underlay" "^2.0.9" + "@spectrum-css/vars" "^3.0.1" + dayjs "^1.10.4" + svelte-flatpickr "^3.1.0" + svelte-portal "^1.0.0" + +"@budibase/handlebars-helpers@^0.11.4": + version "0.11.5" + resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.5.tgz#e9cc90a44e94ad536992cf10906829b633e94bc5" + integrity sha512-ZxpyNtTHxS8Y+yTicbgWvYDAydooUSjOf3Y+wmTE2d4NpDgO0g0IjepLfZV+KASv9XBr//ylJdjE4hClX9NTFw== + dependencies: + array-sort "^1.0.0" + define-property "^2.0.2" + extend-shallow "^3.0.2" + "falsey" "^1.0.0" + for-in "^1.0.2" + get-object "^0.2.0" + get-value "^3.0.1" + handlebars "^4.7.7" + handlebars-utils "^1.0.6" + has-value "^2.0.2" + helper-date "^1.0.1" + helper-markdown "^1.0.0" + helper-md "^0.2.2" + html-tag "^2.0.0" + is-even "^1.0.0" + is-glob "^4.0.1" + kind-of "^6.0.3" + micromatch "^3.1.5" + relative "^3.0.2" + striptags "^3.1.1" + to-gfm-code-block "^0.1.1" + year "^0.2.1" + "@budibase/standard-components@^0.9.124": version "0.9.125" resolved "https://registry.yarnpkg.com/@budibase/standard-components/-/standard-components-0.9.125.tgz#92244f009ce22f105694ca85fdc78923f8bcba11" @@ -95,6 +172,17 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" +"@budibase/string-templates@^0.9.125-alpha.5": + version "0.9.128" + resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.128.tgz#50ee46dc0d726d481bd5139cd0b38364649a8463" + integrity sha512-4TzmnX2o5S2cts08ukB86El4wYm7cHuV2t6a7yDMGPe1mWeKP1WEtVF6rKhXEdbPTiotW8oYondOlgOP7DT9lA== + dependencies: + "@budibase/handlebars-helpers" "^0.11.4" + dayjs "^1.10.4" + handlebars "^4.7.6" + handlebars-utils "^1.0.6" + lodash "^4.17.20" + "@rollup/plugin-alias@^3.1.5": version "3.1.5" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-3.1.5.tgz#73356a3a1eab2e1e2fd952f9f53cd89fc740d952" @@ -453,13 +541,42 @@ apexcharts@^3.19.2, apexcharts@^3.22.1: svg.resize.js "^1.4.3" svg.select.js "^3.0.1" -argparse@^1.0.7: +argparse@^1.0.10, argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-sort@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" + integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== + dependencies: + default-compare "^1.0.0" + get-value "^2.0.6" + kind-of "^5.0.2" + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -482,11 +599,28 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autolinker@~0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47" + integrity sha1-BlK0kYgYefB3XazgzcoyM5QqTkc= + dependencies: + gulp-header "^1.7.1" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -502,6 +636,19 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -544,6 +691,22 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -648,6 +811,21 @@ builtin-modules@^3.1.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -725,6 +903,16 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + clone@~0.1.9: version "0.1.19" resolved "https://registry.yarnpkg.com/clone/-/clone-0.1.19.tgz#613fb68639b26a494ac53253e15b1a6bd88ada85" @@ -739,6 +927,14 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + color-convert@^1.9.0, color-convert@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -801,6 +997,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -816,13 +1017,18 @@ concat-stream@^1.4.4: readable-stream "^2.2.2" typedarray "^0.0.6" -concat-with-sourcemaps@^1.1.0: +concat-with-sourcemaps@*, concat-with-sourcemaps@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== dependencies: source-map "^0.6.1" +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1059,16 +1265,42 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date.js@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/date.js/-/date.js-0.3.3.tgz#ef1e92332f507a638795dbb985e951882e50bbda" + integrity sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw== + dependencies: + debug "~3.1.0" + dayjs@^1.10.4, dayjs@^1.10.5: version "1.10.6" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63" integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw== +debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + decimal.js@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -1079,6 +1311,13 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" + integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== + dependencies: + kind-of "^5.0.2" + deferred-leveldown@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz#2cef1f111e1c57870d8bbb8af2650e587cd2f5b4" @@ -1093,6 +1332,28 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1186,6 +1447,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -1311,11 +1577,53 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -1326,6 +1634,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +"falsey@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/falsey/-/falsey-1.0.0.tgz#71bdd775c24edad9f2f5c015ce8be24400bb5d7d" + integrity sha512-zMDNZ/Ipd8MY0+346CPvhzP1AsiVyNfTOayJza4reAIWf72xbkuFUDcJNxSAsQE1b9Bu0wijKb8Ngnh/a7fI5w== + fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1341,11 +1654,26 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + flatpickr@^4.5.2: version "4.6.9" resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.9.tgz#9a13383e8a6814bda5d232eae3fcdccb97dc1499" integrity sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw== +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + foreach@~2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -1365,6 +1693,18 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -1412,6 +1752,26 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-object@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c" + integrity sha1-2S/31RkMZFMM2gVD2sY6PUf+jAw= + dependencies: + is-number "^2.0.2" + isobject "^0.2.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +get-value@^3.0.0, get-value@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" + integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== + dependencies: + isobject "^3.0.1" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1436,6 +1796,35 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +gulp-header@^1.7.1: + version "1.8.12" + resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" + integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ== + dependencies: + concat-with-sourcemaps "*" + lodash.template "^4.4.0" + through2 "^2.0.0" + +handlebars-utils@^1.0.2, handlebars-utils@^1.0.4, handlebars-utils@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/handlebars-utils/-/handlebars-utils-1.0.6.tgz#cb9db43362479054782d86ffe10f47abc76357f9" + integrity sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw== + dependencies: + kind-of "^6.0.0" + typeof-article "^0.1.1" + +handlebars@^4.7.6, handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -1469,6 +1858,52 @@ has-symbols@^1.0.1, has-symbols@^1.0.2: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-value@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-2.0.2.tgz#d0f12e8780ba8e90e66ad1a21c707fdb67c25658" + integrity sha512-ybKOlcRsK2MqrM3Hmz/lQxXHZ6ejzSPzpNabKB45jb5qDgJvKPa3SdapTsTLwEb9WltgWpOmNax7i+DzNOk4TA== + dependencies: + get-value "^3.0.0" + has-values "^2.0.1" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has-values@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-2.0.1.tgz#3876200ff86d8a8546a9264a952c17d5fc17579d" + integrity sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w== + dependencies: + kind-of "^6.0.2" + has@^1.0.0, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -1493,11 +1928,44 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +helper-date@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/helper-date/-/helper-date-1.0.1.tgz#12fedea3ad8e44a7ca4c4efb0ff4104a5120cffb" + integrity sha512-wU3VOwwTJvGr/w5rZr3cprPHO+hIhlblTJHD6aFBrKLuNbf4lAmkawd2iK3c6NbJEvY7HAmDpqjOFSI5/+Ey2w== + dependencies: + date.js "^0.3.1" + handlebars-utils "^1.0.4" + moment "^2.18.1" + +helper-markdown@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/helper-markdown/-/helper-markdown-1.0.0.tgz#ee7e9fc554675007d37eb90f7853b13ce74f3e10" + integrity sha512-AnDqMS4ejkQK0MXze7pA9TM3pu01ZY+XXsES6gEE0RmCGk5/NIfvTn0NmItfyDOjRAzyo9z6X7YHbHX4PzIvOA== + dependencies: + handlebars-utils "^1.0.2" + highlight.js "^9.12.0" + remarkable "^1.7.1" + +helper-md@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/helper-md/-/helper-md-0.2.2.tgz#c1f59d7e55bbae23362fd8a0e971607aec69d41f" + integrity sha1-wfWdflW7riM2L9ig6XFgeuxp1B8= + dependencies: + ent "^2.2.0" + extend-shallow "^2.0.1" + fs-exists-sync "^0.1.0" + remarkable "^1.6.2" + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +highlight.js@^9.12.0: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -1529,6 +1997,14 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-tag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tag/-/html-tag-2.0.0.tgz#36c3bc8d816fd30b570d5764a497a641640c2fed" + integrity sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g== + dependencies: + is-self-closing "^1.0.1" + kind-of "^6.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -1623,6 +2099,20 @@ is-absolute-url@^2.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -1645,6 +2135,11 @@ is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.0" +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-callable@^1.1.4, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" @@ -1669,16 +2164,79 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + is-date-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + is-directory@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= +is-even@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-even/-/is-even-1.0.0.tgz#76b5055fbad8d294a86b6a949015e1c97b717c06" + integrity sha1-drUFX7rY0pSoa2qUkBXhyXtxfAY= + dependencies: + is-odd "^0.1.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -1694,6 +2252,20 @@ is-number-object@^1.0.4: resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== +is-number@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + is-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" @@ -1704,6 +2276,20 @@ is-object@~0.1.2: resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7" integrity sha1-AO+8CIFsM8/ErIJR0TLhDcZQmNc= +is-odd@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-0.1.2.tgz#bc573b5ce371ef2aad6e6f49799b72bef13978a7" + integrity sha1-vFc7XONx7yqtbm9JeZtyvvE5eKc= + dependencies: + is-number "^3.0.0" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + is-potential-custom-element-name@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" @@ -1729,6 +2315,13 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== +is-self-closing@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-self-closing/-/is-self-closing-1.0.1.tgz#5f406b527c7b12610176320338af0fa3896416e4" + integrity sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg== + dependencies: + self-closing-tags "^1.0.1" + is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" @@ -1753,6 +2346,11 @@ is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + is@~0.2.6: version "0.2.7" resolved "https://registry.yarnpkg.com/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562" @@ -1763,7 +2361,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -1773,6 +2371,23 @@ isbuffer@~0.0.0: resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b" integrity sha1-OMFG2d9Si4v5sHAcPUPPEt8/w5s= +isobject@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-0.2.0.tgz#a3432192f39b910b5f02cc989487836ec70aa85e" + integrity sha1-o0MhkvObkQtfAsyYlIeDbscKqF4= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -1886,6 +2501,30 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0, kind-of@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + level-blobs@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/level-blobs/-/level-blobs-0.1.7.tgz#9ab9b97bb99f1edbf9f78a3433e21ed56386bdaf" @@ -1993,6 +2632,11 @@ loader-utils@^1.1.0: emojis-list "^3.0.0" json5 "^1.0.1" +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -2008,12 +2652,27 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash.template@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.19: +lodash@^4.17.19, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -2037,6 +2696,18 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -2061,6 +2732,25 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +micromatch@^3.1.5: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -2103,6 +2793,14 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -2110,6 +2808,16 @@ mkdirp@~0.5.1: dependencies: minimist "^1.2.5" +moment@^2.18.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + nanoid@^2.1.0: version "2.1.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" @@ -2120,6 +2828,28 @@ nanoid@^3.1.22: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + node-releases@^1.1.71: version "1.1.73" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" @@ -2147,6 +2877,15 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + object-inspect@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" @@ -2171,6 +2910,13 @@ object-keys@~0.4.0: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" @@ -2190,6 +2936,13 @@ object.getownpropertydescriptors@^2.1.0: define-properties "^1.1.3" es-abstract "^1.18.0-next.2" +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + object.values@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" @@ -2285,6 +3038,11 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -2326,6 +3084,11 @@ pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + postcss-calc@^7.0.1: version "7.0.5" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" @@ -2757,7 +3520,7 @@ readable-stream@^1.0.26-4: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.2.2: +readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -2789,11 +3552,44 @@ readable-stream@~1.0.26, readable-stream@~1.0.26-4: isarray "0.0.1" string_decoder "~0.10.x" +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + regexparam@1.3.0, regexparam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f" integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g== +relative@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" + integrity sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8= + dependencies: + isobject "^2.0.0" + +remarkable@^1.6.2, remarkable@^1.7.1: + version "1.7.4" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" + integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg== + dependencies: + argparse "^1.0.10" + autolinker "~0.28.0" + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + request-promise-core@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" @@ -2856,6 +3652,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + resolve@^1.17.0, resolve@^1.19.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" @@ -2864,6 +3665,11 @@ resolve@^1.17.0, resolve@^1.19.0: is-core-module "^2.2.0" path-parse "^1.0.6" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -2992,6 +3798,13 @@ safe-identifier@^0.4.2: resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -3009,6 +3822,11 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" +self-closing-tags@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/self-closing-tags/-/self-closing-tags-1.0.1.tgz#6c5fa497994bb826b484216916371accee490a5d" + integrity sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA== + semver@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52" @@ -3021,6 +3839,16 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -3048,6 +3876,47 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" @@ -3056,6 +3925,16 @@ source-map-support@~0.5.19: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -3071,6 +3950,13 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -3096,6 +3982,14 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -3146,6 +4040,11 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +striptags@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052" + integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw== + style-inject@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" @@ -3300,11 +4199,49 @@ terser@^5.0.0: source-map "~0.7.2" source-map-support "~0.5.19" +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +to-gfm-code-block@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz#25d045a5fae553189e9637b590900da732d8aa82" + integrity sha1-JdBFpfrlUxielje1kJANpzLYqoI= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -3358,6 +4295,18 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typeof-article@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/typeof-article/-/typeof-article-0.1.1.tgz#9f07e733c3fbb646ffa9e61c08debacd460e06af" + integrity sha1-nwfnM8P7tkb/qeYcCN66zUYOBq8= + dependencies: + kind-of "^3.1.0" + +uglify-js@^3.1.4: + version "3.14.2" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99" + integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A== + unbox-primitive@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -3368,6 +4317,16 @@ unbox-primitive@^1.0.0: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -3388,6 +4347,14 @@ unquote@~1.1.1: resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + uri-js@^4.2.2: version "4.4.0" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" @@ -3395,6 +4362,16 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -3495,6 +4472,11 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -3540,7 +4522,17 @@ xtend@~3.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo= +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +year@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/year/-/year-0.2.1.tgz#4083ae520a318b23ec86037f3000cb892bdf9bb0" + integrity sha1-QIOuUgoxiyPshgN/MADLiSvfm7A= diff --git a/packages/server/package.json b/packages/server/package.json index 4bcae1c619..4c5c99e7e5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "description": "Budibase Web Server", "main": "src/index.js", "repository": { @@ -61,9 +61,9 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.125-alpha.2", - "@budibase/client": "^0.9.125-alpha.2", - "@budibase/string-templates": "^0.9.125-alpha.2", + "@budibase/auth": "^0.9.125-alpha.5", + "@budibase/client": "^0.9.125-alpha.5", + "@budibase/string-templates": "^0.9.125-alpha.5", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", @@ -109,7 +109,6 @@ "to-json-schema": "0.2.5", "uuid": "3.3.2", "validate.js": "0.13.1", - "worker-farm": "1.7.0", "yargs": "13.2.4", "zlib": "1.0.5" }, diff --git a/packages/server/src/api/controllers/automation.js b/packages/server/src/api/controllers/automation.js index 2d164b415d..60a0928e33 100644 --- a/packages/server/src/api/controllers/automation.js +++ b/packages/server/src/api/controllers/automation.js @@ -1,12 +1,14 @@ const CouchDB = require("../../db") const actions = require("../../automations/actions") -const logic = require("../../automations/logic") const triggers = require("../../automations/triggers") -const webhooks = require("./webhook") const { getAutomationParams, generateAutomationID } = require("../../db/utils") - -const WH_STEP_ID = triggers.BUILTIN_DEFINITIONS.WEBHOOK.stepId -const CRON_STEP_ID = triggers.BUILTIN_DEFINITIONS.CRON.stepId +const { + checkForWebhooks, + updateTestHistory, +} = require("../../automations/utils") +const { deleteEntityMetadata } = require("../../utilities") +const { MetadataTypes } = require("../../constants") +const { setTestFlag, clearTestFlag } = require("../../utilities/redis") /************************* * * @@ -14,6 +16,19 @@ const CRON_STEP_ID = triggers.BUILTIN_DEFINITIONS.CRON.stepId * * *************************/ +async function cleanupAutomationMetadata(appId, automationId) { + await deleteEntityMetadata( + appId, + MetadataTypes.AUTOMATION_TEST_INPUT, + automationId + ) + await deleteEntityMetadata( + appId, + MetadataTypes.AUTOMATION_TEST_HISTORY, + automationId + ) +} + function cleanAutomationInputs(automation) { if (automation == null) { return automation @@ -21,6 +36,10 @@ function cleanAutomationInputs(automation) { let steps = automation.definition.steps let trigger = automation.definition.trigger let allSteps = [...steps, trigger] + // live is not a property used anymore + if (automation.live != null) { + delete automation.live + } for (let step of allSteps) { if (step == null) { continue @@ -34,119 +53,6 @@ function cleanAutomationInputs(automation) { return automation } -/** - * This function handles checking of any cron jobs need to be created or deleted for automations. - * @param {string} appId The ID of the app in which we are checking for webhooks - * @param {object|undefined} oldAuto The old automation object if updating/deleting - * @param {object|undefined} newAuto The new automation object if creating/updating - */ -async function checkForCronTriggers({ appId, oldAuto, newAuto }) { - const oldTrigger = oldAuto ? oldAuto.definition.trigger : null - const newTrigger = newAuto ? newAuto.definition.trigger : null - function isCronTrigger(auto) { - return ( - auto && - auto.definition.trigger && - auto.definition.trigger.stepId === CRON_STEP_ID - ) - } - - const isLive = auto => auto && auto.live - - const cronTriggerRemoved = - isCronTrigger(oldAuto) && !isCronTrigger(newAuto) && oldTrigger.cronJobId - const cronTriggerDeactivated = !isLive(newAuto) && isLive(oldAuto) - - const cronTriggerActivated = isLive(newAuto) && !isLive(oldAuto) - - if (cronTriggerRemoved || (cronTriggerDeactivated && oldTrigger.cronJobId)) { - await triggers.automationQueue.removeRepeatableByKey(oldTrigger.cronJobId) - } - // need to create cron job - else if (isCronTrigger(newAuto) && cronTriggerActivated) { - const job = await triggers.automationQueue.add( - { - automation: newAuto, - event: { appId, timestamp: Date.now() }, - }, - { repeat: { cron: newTrigger.inputs.cron } } - ) - // Assign cron job ID from bull so we can remove it later if the cron trigger is removed - newTrigger.cronJobId = job.id - } - return newAuto -} - -/** - * This function handles checking if any webhooks need to be created or deleted for automations. - * @param {string} appId The ID of the app in which we are checking for webhooks - * @param {object|undefined} oldAuto The old automation object if updating/deleting - * @param {object|undefined} newAuto The new automation object if creating/updating - * @returns {Promise} After this is complete the new automation object may have been updated and should be - * written to DB (this does not write to DB as it would be wasteful to repeat). - */ -async function checkForWebhooks({ appId, oldAuto, newAuto }) { - const oldTrigger = oldAuto ? oldAuto.definition.trigger : null - const newTrigger = newAuto ? newAuto.definition.trigger : null - const triggerChanged = - oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id - function isWebhookTrigger(auto) { - return ( - auto && - auto.definition.trigger && - auto.definition.trigger.stepId === WH_STEP_ID - ) - } - // need to delete webhook - if ( - isWebhookTrigger(oldAuto) && - (!isWebhookTrigger(newAuto) || triggerChanged) && - oldTrigger.webhookId - ) { - try { - let db = new CouchDB(appId) - // need to get the webhook to get the rev - const webhook = await db.get(oldTrigger.webhookId) - const ctx = { - appId, - params: { id: webhook._id, rev: webhook._rev }, - } - // might be updating - reset the inputs to remove the URLs - if (newTrigger) { - delete newTrigger.webhookId - newTrigger.inputs = {} - } - await webhooks.destroy(ctx) - } catch (err) { - // don't worry about not being able to delete, if it doesn't exist all good - } - } - // need to create webhook - if ( - (!isWebhookTrigger(oldAuto) || triggerChanged) && - isWebhookTrigger(newAuto) - ) { - const ctx = { - appId, - request: { - body: new webhooks.Webhook( - "Automation webhook", - webhooks.WebhookType.AUTOMATION, - newAuto._id - ), - }, - } - await webhooks.save(ctx) - const id = ctx.body.webhook._id - newTrigger.webhookId = id - newTrigger.inputs = { - schemaUrl: `api/webhooks/schema/${appId}/${id}`, - triggerUrl: `api/webhooks/trigger/${appId}/${id}`, - } - } - return newAuto -} - exports.create = async function (ctx) { const db = new CouchDB(ctx.appId) let automation = ctx.request.body @@ -165,10 +71,6 @@ exports.create = async function (ctx) { appId: ctx.appId, newAuto: automation, }) - automation = await checkForCronTriggers({ - appId: ctx.appId, - newAuto: automation, - }) const response = await db.put(automation) automation._rev = response.rev @@ -193,14 +95,26 @@ exports.update = async function (ctx) { oldAuto: oldAutomation, newAuto: automation, }) - automation = await checkForCronTriggers({ - appId: ctx.appId, - oldAuto: oldAutomation, - newAuto: automation, - }) const response = await db.put(automation) automation._rev = response.rev + const oldAutoTrigger = + oldAutomation && oldAutomation.definition.trigger + ? oldAutomation.definition.trigger + : {} + const newAutoTrigger = + automation && automation.definition.trigger + ? automation.definition.trigger + : {} + // trigger has been updated, remove the test inputs + if (oldAutoTrigger.id !== newAutoTrigger.id) { + await deleteEntityMetadata( + ctx.appId, + MetadataTypes.AUTOMATION_TEST_INPUT, + automation._id + ) + } + ctx.status = 200 ctx.body = { message: `Automation ${automation._id} updated successfully.`, @@ -229,35 +143,29 @@ exports.find = async function (ctx) { exports.destroy = async function (ctx) { const db = new CouchDB(ctx.appId) - const oldAutomation = await db.get(ctx.params.id) + const automationId = ctx.params.id + const oldAutomation = await db.get(automationId) await checkForWebhooks({ appId: ctx.appId, oldAuto: oldAutomation, }) - await checkForCronTriggers({ - appId: ctx.appId, - oldAuto: oldAutomation, - }) - ctx.body = await db.remove(ctx.params.id, ctx.params.rev) + // delete metadata first + await cleanupAutomationMetadata(ctx.appId, automationId) + ctx.body = await db.remove(automationId, ctx.params.rev) } exports.getActionList = async function (ctx) { - ctx.body = actions.DEFINITIONS + ctx.body = actions.ACTION_DEFINITIONS } exports.getTriggerList = async function (ctx) { - ctx.body = triggers.BUILTIN_DEFINITIONS -} - -exports.getLogicList = async function (ctx) { - ctx.body = logic.BUILTIN_DEFINITIONS + ctx.body = triggers.TRIGGER_DEFINITIONS } module.exports.getDefinitionList = async function (ctx) { ctx.body = { - logic: logic.BUILTIN_DEFINITIONS, - trigger: triggers.BUILTIN_DEFINITIONS, - action: actions.DEFINITIONS, + trigger: triggers.TRIGGER_DEFINITIONS, + action: actions.ACTION_DEFINITIONS, } } @@ -268,15 +176,37 @@ module.exports.getDefinitionList = async function (ctx) { *********************/ exports.trigger = async function (ctx) { - const db = new CouchDB(ctx.appId) + const appId = ctx.appId + const db = new CouchDB(appId) let automation = await db.get(ctx.params.id) await triggers.externalTrigger(automation, { ...ctx.request.body, - appId: ctx.appId, + appId, }) - ctx.status = 200 ctx.body = { message: `Automation ${automation._id} has been triggered.`, automation, } } + +exports.test = async function (ctx) { + const appId = ctx.appId + const db = new CouchDB(appId) + let automation = await db.get(ctx.params.id) + await setTestFlag(automation._id) + const response = await triggers.externalTrigger( + automation, + { + ...ctx.request.body, + appId, + }, + { getResponses: true } + ) + // save a test history run + await updateTestHistory(ctx.appId, automation, { + ...ctx.request.body, + occurredAt: new Date().getTime(), + }) + await clearTestFlag(automation._id) + ctx.body = response +} diff --git a/packages/server/src/api/controllers/deploy/index.js b/packages/server/src/api/controllers/deploy/index.js index 4608ca6342..08f9072138 100644 --- a/packages/server/src/api/controllers/deploy/index.js +++ b/packages/server/src/api/controllers/deploy/index.js @@ -1,7 +1,11 @@ const CouchDB = require("../../../db") const Deployment = require("./Deployment") const { Replication } = require("@budibase/auth/db") -const { DocumentTypes } = require("../../../db/utils") +const { DocumentTypes, getAutomationParams } = require("../../../db/utils") +const { + disableAllCrons, + enableCronTrigger, +} = require("../../../automations/utils") // the max time we can wait for an invalidation to complete before considering it failed const MAX_PENDING_TIME_MS = 30 * 60000 @@ -58,6 +62,23 @@ async function storeDeploymentHistory(deployment) { return deployment } +async function initDeployedApp(prodAppId) { + const db = new CouchDB(prodAppId) + const automations = ( + await db.allDocs( + getAutomationParams(null, { + include_docs: true, + }) + ) + ).rows.map(row => row.doc) + const promises = [] + await disableAllCrons(prodAppId) + for (let automation of automations) { + promises.push(enableCronTrigger(prodAppId, automation)) + } + await Promise.all(promises) +} + async function deployApp(deployment) { try { const productionAppId = deployment.appId.replace("_dev", "") @@ -85,6 +106,7 @@ async function deployApp(deployment) { }, }) + await initDeployedApp(productionAppId) deployment.setStatus(DeploymentStatus.SUCCESS) await storeDeploymentHistory(deployment) } catch (err) { diff --git a/packages/server/src/api/controllers/metadata.js b/packages/server/src/api/controllers/metadata.js new file mode 100644 index 0000000000..75236650fd --- /dev/null +++ b/packages/server/src/api/controllers/metadata.js @@ -0,0 +1,46 @@ +const { MetadataTypes } = require("../../constants") +const CouchDB = require("../../db") +const { generateMetadataID } = require("../../db/utils") +const { saveEntityMetadata, deleteEntityMetadata } = require("../../utilities") + +exports.getTypes = async ctx => { + ctx.body = { + types: MetadataTypes, + } +} + +exports.saveMetadata = async ctx => { + const { type, entityId } = ctx.params + if (type === MetadataTypes.AUTOMATION_TEST_HISTORY) { + ctx.throw(400, "Cannot save automation history type") + } + ctx.body = await saveEntityMetadata( + ctx.appId, + type, + entityId, + ctx.request.body + ) +} + +exports.deleteMetadata = async ctx => { + const { type, entityId } = ctx.params + await deleteEntityMetadata(ctx.appId, type, entityId) + ctx.body = { + message: "Metadata deleted successfully", + } +} + +exports.getMetadata = async ctx => { + const { type, entityId } = ctx.params + const db = new CouchDB(ctx.appId) + const id = generateMetadataID(type, entityId) + try { + ctx.body = await db.get(id) + } catch (err) { + if (err.status === 404) { + ctx.body = {} + } else { + ctx.throw(err.status, err) + } + } +} diff --git a/packages/server/src/api/controllers/query.js b/packages/server/src/api/controllers/query.js index d584b73e20..737c8c9b01 100644 --- a/packages/server/src/api/controllers/query.js +++ b/packages/server/src/api/controllers/query.js @@ -160,8 +160,6 @@ exports.execute = async function (ctx) { ) const integration = new Integration(datasource.config) - console.log(query) - // ctx.body = {} // call the relevant CRUD method on the integration class ctx.body = formatResponse(await integration[query.queryVerb](enrichedQuery)) // cleanup diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 84e06f5855..2299a20580 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -23,6 +23,19 @@ const CALCULATION_TYPES = { STATS: "stats", } +async function storeResponse(ctx, db, row, oldTable, table) { + row.type = "row" + const response = await db.put(row) + // don't worry about rev, tables handle rev/lastID updates + if (!isEqual(oldTable, table)) { + await db.put(table) + } + row._rev = response.rev + // process the row before return, to include relationships + row = await outputProcessing(ctx, table, row, { squash: false }) + return { row, table } +} + exports.patch = async ctx => { const appId = ctx.appId const db = new CouchDB(appId) @@ -77,14 +90,7 @@ exports.patch = async ctx => { return { row: ctx.body, table } } - const response = await db.put(row) - // don't worry about rev, tables handle rev/lastID updates - if (!isEqual(dbTable, table)) { - await db.put(table) - } - row._rev = response.rev - row.type = "row" - return { row, table } + return storeResponse(ctx, db, row, dbTable, table) } exports.save = async function (ctx) { @@ -118,14 +124,7 @@ exports.save = async function (ctx) { table, }) - row.type = "row" - const response = await db.put(row) - // don't worry about rev, tables handle rev/lastID updates - if (!isEqual(dbTable, table)) { - await db.put(table) - } - row._rev = response.rev - return { row, table } + return storeResponse(ctx, db, row, dbTable, table) } exports.fetchView = async ctx => { @@ -221,34 +220,47 @@ exports.destroy = async function (ctx) { const appId = ctx.appId const db = new CouchDB(appId) const { _id, _rev } = ctx.request.body - const row = await db.get(_id) + let row = await db.get(_id) if (row.tableId !== ctx.params.tableId) { throw "Supplied tableId doesn't match the row's tableId" } + const table = await db.get(row.tableId) + // update the row to include full relationships before deleting them + row = await outputProcessing(ctx, table, row, { squash: false }) + // now remove the relationships await linkRows.updateLinks({ appId, eventType: linkRows.EventType.ROW_DELETE, row, tableId: row.tableId, }) + + let response if (ctx.params.tableId === InternalTables.USER_METADATA) { ctx.params = { id: _id, } await userController.destroyMetadata(ctx) - return { response: ctx.body, row } + response = ctx.body } else { - const response = await db.remove(_id, _rev) - return { response, row } + response = await db.remove(_id, _rev) } + return { response, row } } exports.bulkDestroy = async ctx => { const appId = ctx.appId - const { rows } = ctx.request.body const db = new CouchDB(appId) + const tableId = ctx.params.tableId + const table = await db.get(tableId) + let { rows } = ctx.request.body + // before carrying out any updates, make sure the rows are ready to be returned + // they need to be the full rows (including previous relationships) for automations + rows = await outputProcessing(ctx, table, rows, { squash: false }) + + // remove the relationships first let updates = rows.map(row => linkRows.updateLinks({ appId, @@ -257,8 +269,7 @@ exports.bulkDestroy = async ctx => { tableId: row.tableId, }) ) - // TODO remove special user case in future - if (ctx.params.tableId === InternalTables.USER_METADATA) { + if (tableId === InternalTables.USER_METADATA) { updates = updates.concat( rows.map(row => { ctx.params = { diff --git a/packages/server/src/api/routes/automation.js b/packages/server/src/api/routes/automation.js index c7b674b4e4..599b16b583 100644 --- a/packages/server/src/api/routes/automation.js +++ b/packages/server/src/api/routes/automation.js @@ -9,6 +9,10 @@ const { } = require("@budibase/auth/permissions") const Joi = require("joi") const { bodyResource, paramResource } = require("../../middleware/resourceId") +const { + middleware: appInfoMiddleware, + AppType, +} = require("../../middleware/appInfo") const router = Router() @@ -22,7 +26,6 @@ function generateStepSchema(allowStepTypes) { tagline: Joi.string().required(), icon: Joi.string().required(), params: Joi.object(), - // TODO: validate args a bit more deeply args: Joi.object(), type: Joi.string().required().valid(...allowStepTypes), }).unknown(true) @@ -31,7 +34,6 @@ function generateStepSchema(allowStepTypes) { function generateValidator(existing = false) { // prettier-ignore return joiValidator.body(Joi.object({ - live: Joi.bool(), _id: existing ? Joi.string().required() : Joi.string(), _rev: existing ? Joi.string().required() : Joi.string(), name: Joi.string().required(), @@ -54,11 +56,6 @@ router authorized(BUILDER), controller.getActionList ) - .get( - "/api/automations/logic/list", - authorized(BUILDER), - controller.getLogicList - ) .get( "/api/automations/definitions/list", authorized(BUILDER), @@ -84,17 +81,25 @@ router generateValidator(false), controller.create ) - .post( - "/api/automations/:id/trigger", - paramResource("id"), - authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), - controller.trigger - ) .delete( "/api/automations/:id/:rev", paramResource("id"), authorized(BUILDER), controller.destroy ) + .post( + "/api/automations/:id/trigger", + appInfoMiddleware({ appType: AppType.PROD }), + paramResource("id"), + authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), + controller.trigger + ) + .post( + "/api/automations/:id/test", + appInfoMiddleware({ appType: AppType.DEV }), + paramResource("id"), + authorized(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE), + controller.test + ) module.exports = router diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index 0b09a78bb8..2e1353df98 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -22,6 +22,7 @@ const datasourceRoutes = require("./datasource") const queryRoutes = require("./query") const hostingRoutes = require("./hosting") const backupRoutes = require("./backup") +const metadataRoutes = require("./metadata") const devRoutes = require("./dev") exports.mainRoutes = [ @@ -46,6 +47,7 @@ exports.mainRoutes = [ queryRoutes, hostingRoutes, backupRoutes, + metadataRoutes, devRoutes, // these need to be handled last as they still use /api/:tableId // this could be breaking as koa may recognise other routes as this diff --git a/packages/server/src/api/routes/metadata.js b/packages/server/src/api/routes/metadata.js new file mode 100644 index 0000000000..f4eba64358 --- /dev/null +++ b/packages/server/src/api/routes/metadata.js @@ -0,0 +1,38 @@ +const Router = require("@koa/router") +const controller = require("../controllers/metadata") +const { + middleware: appInfoMiddleware, + AppType, +} = require("../../middleware/appInfo") +const authorized = require("../../middleware/authorized") +const { BUILDER } = require("@budibase/auth/permissions") + +const router = Router() + +router + .post( + "/api/metadata/:type/:entityId", + authorized(BUILDER), + appInfoMiddleware({ appType: AppType.DEV }), + controller.saveMetadata + ) + .delete( + "/api/metadata/:type/:entityId", + authorized(BUILDER), + appInfoMiddleware({ appType: AppType.DEV }), + controller.deleteMetadata + ) + .get( + "/api/metadata/type", + authorized(BUILDER), + appInfoMiddleware({ appType: AppType.DEV }), + controller.getTypes + ) + .get( + "/api/metadata/:type/:entityId", + authorized(BUILDER), + appInfoMiddleware({ appType: AppType.DEV }), + controller.getMetadata + ) + +module.exports = router diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js index 5654c14c17..c412c34fdc 100644 --- a/packages/server/src/api/routes/tests/automation.spec.js +++ b/packages/server/src/api/routes/tests/automation.spec.js @@ -2,6 +2,7 @@ const { checkBuilderEndpoint, getAllTableRows, clearAllAutomations, + testAutomation, } = require("./utilities/TestFunctions") const setup = require("./utilities") const { basicAutomation } = setup.structures @@ -10,7 +11,6 @@ const MAX_RETRIES = 4 let ACTION_DEFINITIONS = {} let TRIGGER_DEFINITIONS = {} -let LOGIC_DEFINITIONS = {} describe("/automations", () => { let request = setup.getRequest() @@ -23,15 +23,6 @@ describe("/automations", () => { await config.init() }) - const triggerWorkflow = async automation => { - return await request - .post(`/api/automations/${automation._id}/trigger`) - .send({ name: "Test", description: "TEST" }) - .set(config.defaultHeaders()) - .expect('Content-Type', /json/) - .expect(200) - } - describe("get definitions", () => { it("returns a list of definitions for actions", async () => { const res = await request @@ -44,7 +35,7 @@ describe("/automations", () => { ACTION_DEFINITIONS = res.body }) - it("returns a list of definitions for triggers", async () => { + it("returns a list of definitions for triggerInfo", async () => { const res = await request .get(`/api/automations/trigger/list`) .set(config.defaultHeaders()) @@ -55,17 +46,6 @@ describe("/automations", () => { TRIGGER_DEFINITIONS = res.body }) - it("returns a list of definitions for actions", async () => { - const res = await request - .get(`/api/automations/logic/list`) - .set(config.defaultHeaders()) - .expect('Content-Type', /json/) - .expect(200) - - expect(Object.keys(res.body).length).not.toEqual(0) - LOGIC_DEFINITIONS = res.body - }) - it("returns all of the definitions in one", async () => { const res = await request .get(`/api/automations/definitions/list`) @@ -75,7 +55,6 @@ describe("/automations", () => { expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(Object.keys(ACTION_DEFINITIONS).length) expect(Object.keys(res.body.trigger).length).toEqual(Object.keys(TRIGGER_DEFINITIONS).length) - expect(Object.keys(res.body.logic).length).toEqual(Object.keys(LOGIC_DEFINITIONS).length) }) }) @@ -168,14 +147,13 @@ describe("/automations", () => { automation.definition.steps[0].inputs.row.tableId = table._id automation = await config.createAutomation(automation) await setup.delay(500) - const res = await triggerWorkflow(automation) + const res = await testAutomation(config, automation) // 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 // 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 for (let tries = 0; tries < MAX_RETRIES; tries++) { - expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`) - expect(res.body.automation.name).toEqual(automation.name) + expect(res.body).toBeDefined() await setup.delay(500) let elements = await getAllTableRows(config) // don't test it unless there are values to test diff --git a/packages/server/src/api/routes/tests/metadata.spec.js b/packages/server/src/api/routes/tests/metadata.spec.js new file mode 100644 index 0000000000..5befd492a2 --- /dev/null +++ b/packages/server/src/api/routes/tests/metadata.spec.js @@ -0,0 +1,65 @@ +const { testAutomation } = require("./utilities/TestFunctions") +const setup = require("./utilities") +const { MetadataTypes } = require("../../../constants") + +describe("/metadata", () => { + let request = setup.getRequest() + let config = setup.getConfig() + let automation + + afterAll(setup.afterAll) + + beforeEach(async () => { + await config.init() + automation = await config.createAutomation() + }) + + async function createMetadata(data, type = MetadataTypes.AUTOMATION_TEST_INPUT) { + const res = await request + .post(`/api/metadata/${type}/${automation._id}`) + .send(data) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._rev).toBeDefined() + } + + async function getMetadata(type) { + const res = await request + .get(`/api/metadata/${type}/${automation._id}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + return res.body + } + + describe("save", () => { + it("should be able to save some metadata", async () => { + await createMetadata({ test: "a" }) + const testInput = await getMetadata(MetadataTypes.AUTOMATION_TEST_INPUT) + expect(testInput.test).toBe("a") + }) + + it("should save history metadata on automation run", async () => { + // this should have created some history + await testAutomation(config, automation) + const metadata = await getMetadata(MetadataTypes.AUTOMATION_TEST_HISTORY) + expect(metadata).toBeDefined() + expect(metadata.history.length).toBe(1) + expect(typeof metadata.history[0].occurredAt).toBe("number") + }) + }) + + describe("destroy", () => { + it("should be able to delete some test inputs", async () => { + const res = await request + .delete(`/api/metadata/${MetadataTypes.AUTOMATION_TEST_INPUT}/${automation._id}`) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + expect(res.body.message).toBeDefined() + const metadata = await getMetadata(MetadataTypes.AUTOMATION_TEST_INPUT) + expect(metadata.test).toBeUndefined() + }) + }) +}) \ No newline at end of file diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.js b/packages/server/src/api/routes/tests/utilities/TestFunctions.js index 944d2ac527..87190c3f76 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.js +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.js @@ -101,3 +101,17 @@ exports.checkPermissionsEndpoint = async ({ exports.getDB = config => { return new CouchDB(config.getAppId()) } + +exports.testAutomation = async (config, automation) => { + return await config.request + .post(`/api/automations/${automation._id}/test`) + .send({ + row: { + name: "Test", + description: "TEST", + }, + }) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) +} diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index a32f241aa3..23e05e5e95 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -88,8 +88,8 @@ module.exports = server.listen(env.PORT || 0, async () => { env._set("PORT", server.address().port) eventEmitter.emitPort(env.PORT) fileSystem.init() - await automations.init() await redis.init() + await automations.init() }) process.on("uncaughtException", err => { diff --git a/packages/server/src/automations/actions.js b/packages/server/src/automations/actions.js index b0b5f9f1ba..ef77387a40 100644 --- a/packages/server/src/automations/actions.js +++ b/packages/server/src/automations/actions.js @@ -1,4 +1,3 @@ -const sendgridEmail = require("./steps/sendgridEmail") const sendSmtpEmail = require("./steps/sendSmtpEmail") const createRow = require("./steps/createRow") const updateRow = require("./steps/updateRow") @@ -8,15 +7,14 @@ const bash = require("./steps/bash") const executeQuery = require("./steps/executeQuery") const outgoingWebhook = require("./steps/outgoingWebhook") const serverLog = require("./steps/serverLog") -const env = require("../environment") -const Sentry = require("@sentry/node") -const { - automationInit, - getExternalAutomationStep, -} = require("../utilities/fileSystem") +const discord = require("./steps/discord") +const slack = require("./steps/slack") +const zapier = require("./steps/zapier") +const integromat = require("./steps/integromat") +let filter = require("./steps/filter") +let delay = require("./steps/delay") -const BUILTIN_ACTIONS = { - SEND_EMAIL: sendgridEmail.run, +const ACTION_IMPLS = { SEND_EMAIL_SMTP: sendSmtpEmail.run, CREATE_ROW: createRow.run, UPDATE_ROW: updateRow.run, @@ -26,9 +24,15 @@ const BUILTIN_ACTIONS = { EXECUTE_BASH: bash.run, EXECUTE_QUERY: executeQuery.run, SERVER_LOG: serverLog.run, + DELAY: delay.run, + FILTER: filter.run, + // these used to be lowercase step IDs, maintain for backwards compat + discord: discord.run, + slack: slack.run, + zapier: zapier.run, + integromat: integromat.run, } -const BUILTIN_DEFINITIONS = { - SEND_EMAIL: sendgridEmail.definition, +const ACTION_DEFINITIONS = { SEND_EMAIL_SMTP: sendSmtpEmail.definition, CREATE_ROW: createRow.definition, UPDATE_ROW: updateRow.definition, @@ -38,47 +42,20 @@ const BUILTIN_DEFINITIONS = { EXECUTE_QUERY: executeQuery.definition, EXECUTE_BASH: bash.definition, SERVER_LOG: serverLog.definition, -} - -let MANIFEST = null - -/* istanbul ignore next */ -function buildBundleName(pkgName, version) { - return `${pkgName}@${version}.min.js` + DELAY: delay.definition, + FILTER: filter.definition, + // these used to be lowercase step IDs, maintain for backwards compat + discord: discord.definition, + slack: slack.definition, + zapier: zapier.definition, + integromat: integromat.definition, } /* istanbul ignore next */ -module.exports.getAction = async function (actionName) { - if (BUILTIN_ACTIONS[actionName] != null) { - return BUILTIN_ACTIONS[actionName] +exports.getAction = async function (actionName) { + if (ACTION_IMPLS[actionName] != null) { + return ACTION_IMPLS[actionName] } - // worker pools means that a worker may not have manifest - if (env.isProd() && MANIFEST == null) { - MANIFEST = await module.exports.init() - } - // 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) - return getExternalAutomationStep(pkg.stepId, pkg.version, bundleName) } -module.exports.init = async function () { - try { - MANIFEST = await automationInit() - module.exports.DEFINITIONS = - MANIFEST && MANIFEST.packages - ? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS) - : BUILTIN_DEFINITIONS - } catch (err) { - console.error(err) - Sentry.captureException(err) - } - return MANIFEST -} - -// definitions will have downloaded ones added to it, while builtin won't -module.exports.DEFINITIONS = BUILTIN_DEFINITIONS -module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS +exports.ACTION_DEFINITIONS = ACTION_DEFINITIONS diff --git a/packages/server/src/automations/bullboard.js b/packages/server/src/automations/bullboard.js index 364c4afaa1..1f96b6c4d2 100644 --- a/packages/server/src/automations/bullboard.js +++ b/packages/server/src/automations/bullboard.js @@ -1,14 +1,22 @@ const { createBullBoard } = require("bull-board") const { BullAdapter } = require("bull-board/bullAdapter") -const { getQueues } = require("./triggers") const express = require("express") +const env = require("../environment") +const Queue = env.isTest() + ? require("../utilities/queue/inMemoryQueue") + : require("bull") +const { JobQueues } = require("../constants") +const { utils } = require("@budibase/auth/redis") +const { opts } = utils.getRedisOptions() + +let automationQueue = new Queue(JobQueues.AUTOMATIONS, { redis: opts }) exports.pathPrefix = "/bulladmin" exports.init = () => { const expressApp = express() // Set up queues for bull board admin - const queues = getQueues() + const queues = [automationQueue] const adapters = [] for (let queue of queues) { adapters.push(new BullAdapter(queue)) @@ -18,3 +26,5 @@ exports.init = () => { expressApp.use(exports.pathPrefix, router) return expressApp } + +exports.queue = automationQueue diff --git a/packages/server/src/automations/index.js b/packages/server/src/automations/index.js index 8f30b6b32f..87f35ce763 100644 --- a/packages/server/src/automations/index.js +++ b/packages/server/src/automations/index.js @@ -1,51 +1,17 @@ -const triggers = require("./triggers") -const actions = require("./actions") -const env = require("../environment") -const workerFarm = require("worker-farm") -const singleThread = require("./thread") -const { getAPIKey, update, Properties } = require("../utilities/usageQuota") - -let workers = workerFarm(require.resolve("./thread")) - -function runWorker(job) { - return new Promise((resolve, reject) => { - workers(job, err => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) -} - -async function updateQuota(automation) { - const appId = automation.appId - const apiObj = await getAPIKey(appId) - // this will fail, causing automation to escape if limits reached - await update(apiObj.apiKey, Properties.AUTOMATION, 1) - return apiObj.apiKey -} +const { processEvent } = require("./utils") +const { queue } = require("./bullboard") /** * This module is built purely to kick off the worker farm and manage the inputs/outputs */ -module.exports.init = async function () { - await actions.init() - triggers.automationQueue.process(async job => { - try { - if (env.USE_QUOTAS) { - job.data.automation.apiKey = await updateQuota(job.data.automation) - } - if (env.isProd()) { - await runWorker(job) - } else { - await singleThread(job) - } - } catch (err) { - console.error( - `${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` - ) - } +exports.init = function () { + // this promise will not complete + return queue.process(async job => { + await processEvent(job) }) } + +exports.getQueues = () => { + return [queue] +} +exports.queue = queue diff --git a/packages/server/src/automations/logic.js b/packages/server/src/automations/logic.js deleted file mode 100644 index cbd3d42430..0000000000 --- a/packages/server/src/automations/logic.js +++ /dev/null @@ -1,20 +0,0 @@ -let filter = require("./steps/filter") -let delay = require("./steps/delay") - -let BUILTIN_LOGIC = { - DELAY: delay.run, - FILTER: filter.run, -} - -let BUILTIN_DEFINITIONS = { - DELAY: delay.definition, - FILTER: filter.definition, -} - -module.exports.getLogic = function (logicName) { - if (BUILTIN_LOGIC[logicName] != null) { - return BUILTIN_LOGIC[logicName] - } -} - -module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS diff --git a/packages/server/src/automations/steps/bash.js b/packages/server/src/automations/steps/bash.js index 76d6713c5b..6c44c1bbbb 100644 --- a/packages/server/src/automations/steps/bash.js +++ b/packages/server/src/automations/steps/bash.js @@ -1,12 +1,13 @@ const { execSync } = require("child_process") const { processStringSync } = require("@budibase/string-templates") -module.exports.definition = { +exports.definition = { name: "Bash Scripting", tagline: "Execute a bash command", icon: "ri-terminal-box-line", description: "Run a bash script", type: "ACTION", + internal: true, stepId: "EXECUTE_BASH", inputs: {}, schema: { @@ -24,7 +25,11 @@ module.exports.definition = { properties: { stdout: { type: "string", - description: "Standard output of your bash command or script.", + description: "Standard output of your bash command or script", + }, + success: { + type: "boolean", + description: "Whether the command was successful", }, }, }, @@ -32,7 +37,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, context }) { +exports.run = async function ({ inputs, context }) { if (inputs.code == null) { return { stdout: "Budibase bash automation failed: Invalid inputs", @@ -42,18 +47,20 @@ module.exports.run = async function ({ inputs, context }) { try { const command = processStringSync(inputs.code, context) - let stdout + let stdout, + success = true try { stdout = execSync(command, { timeout: 500 }) } catch (err) { stdout = err.message + success = false } return { stdout, + success, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/createRow.js b/packages/server/src/automations/steps/createRow.js index 889f7e98b9..e3c90fb15b 100644 --- a/packages/server/src/automations/steps/createRow.js +++ b/packages/server/src/automations/steps/createRow.js @@ -3,12 +3,13 @@ const automationUtils = require("../automationUtils") const env = require("../../environment") const usage = require("../../utilities/usageQuota") -module.exports.definition = { +exports.definition = { name: "Create Row", tagline: "Create a {{inputs.enriched.table.name}} row", icon: "ri-save-3-line", description: "Add a row to your database", type: "ACTION", + internal: true, stepId: "CREATE_ROW", inputs: {}, schema: { @@ -42,7 +43,7 @@ module.exports.definition = { }, success: { type: "boolean", - description: "Whether the action was successful", + description: "Whether the row creation was successful", }, id: { type: "string", @@ -58,7 +59,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, appId, apiKey, emitter }) { +exports.run = async function ({ inputs, appId, apiKey, emitter }) { if (inputs.row == null || inputs.row.tableId == null) { return { success: false, @@ -97,7 +98,6 @@ module.exports.run = async function ({ inputs, appId, apiKey, emitter }) { success: ctx.status === 200, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/delay.js b/packages/server/src/automations/steps/delay.js index f653d0a980..899d8f8401 100644 --- a/packages/server/src/automations/steps/delay.js +++ b/packages/server/src/automations/steps/delay.js @@ -1,11 +1,12 @@ let { wait } = require("../../utilities") -module.exports.definition = { +exports.definition = { name: "Delay", icon: "ri-time-line", tagline: "Delay for {{inputs.time}} milliseconds", description: "Delay the automation until an amount of time has passed", stepId: "DELAY", + internal: true, inputs: {}, schema: { inputs: { @@ -17,10 +18,22 @@ module.exports.definition = { }, required: ["time"], }, + outputs: { + properties: { + success: { + type: "boolean", + description: "Whether the delay was successful", + }, + }, + required: ["success"], + }, }, type: "LOGIC", } -module.exports.run = async function delay({ inputs }) { +exports.run = async function delay({ inputs }) { await wait(inputs.time) + return { + success: true, + } } diff --git a/packages/server/src/automations/steps/deleteRow.js b/packages/server/src/automations/steps/deleteRow.js index 94243fa03a..10f39d2d0c 100644 --- a/packages/server/src/automations/steps/deleteRow.js +++ b/packages/server/src/automations/steps/deleteRow.js @@ -2,13 +2,14 @@ const rowController = require("../../api/controllers/row") const env = require("../../environment") const usage = require("../../utilities/usageQuota") -module.exports.definition = { +exports.definition = { description: "Delete a row from your database", icon: "ri-delete-bin-line", name: "Delete Row", tagline: "Delete a {{inputs.enriched.table.name}} row", type: "ACTION", stepId: "DELETE_ROW", + internal: true, inputs: {}, schema: { inputs: { @@ -42,7 +43,7 @@ module.exports.definition = { }, success: { type: "boolean", - description: "Whether the action was successful", + description: "Whether the deletion was successful", }, }, required: ["row", "success"], @@ -50,7 +51,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, appId, apiKey, emitter }) { +exports.run = async function ({ inputs, appId, apiKey, emitter }) { if (inputs.id == null || inputs.revision == null) { return { success: false, @@ -84,7 +85,6 @@ module.exports.run = async function ({ inputs, appId, apiKey, emitter }) { success: ctx.status === 200, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/discord.js b/packages/server/src/automations/steps/discord.js new file mode 100644 index 0000000000..5d225644b1 --- /dev/null +++ b/packages/server/src/automations/steps/discord.js @@ -0,0 +1,83 @@ +const fetch = require("node-fetch") +const { getFetchResponse } = require("./utils") + +const DEFAULT_USERNAME = "Budibase Automate" +const DEFAULT_AVATAR_URL = "https://i.imgur.com/a1cmTKM.png" + +exports.definition = { + name: "Discord Message", + tagline: "Send a message to a Discord server", + description: "Send a message to a Discord server", + icon: "ri-discord-line", + stepId: "discord", + type: "ACTION", + internal: false, + inputs: {}, + schema: { + inputs: { + properties: { + url: { + type: "string", + title: "Discord Webhook URL", + }, + username: { + type: "string", + title: "Bot Name", + }, + avatar_url: { + type: "string", + title: "Bot Avatar URL", + }, + content: { + type: "string", + title: "Message", + }, + }, + required: ["url", "content"], + }, + outputs: { + properties: { + httpStatus: { + type: "number", + description: "The HTTP status code of the request", + }, + response: { + type: "string", + description: "The response from the Discord Webhook", + }, + success: { + type: "boolean", + description: "Whether the message sent successfully", + }, + }, + }, + }, +} + +exports.run = async function ({ inputs }) { + let { url, username, avatar_url, content } = inputs + if (!username) { + username = DEFAULT_USERNAME + } + if (!avatar_url) { + avatar_url = DEFAULT_AVATAR_URL + } + const response = await fetch(url, { + method: "post", + body: JSON.stringify({ + username, + avatar_url, + content, + }), + headers: { + "Content-Type": "application/json", + }, + }) + + const { status, message } = await getFetchResponse(response) + return { + httpStatus: status, + success: status === 200, + response: message, + } +} diff --git a/packages/server/src/automations/steps/executeQuery.js b/packages/server/src/automations/steps/executeQuery.js index d045a75544..d5799a8f7d 100644 --- a/packages/server/src/automations/steps/executeQuery.js +++ b/packages/server/src/automations/steps/executeQuery.js @@ -1,12 +1,14 @@ const queryController = require("../../api/controllers/query") +const { buildCtx } = require("./utils") -module.exports.definition = { +exports.definition = { name: "External Data Connector", tagline: "Execute Data Connector", icon: "ri-database-2-line", description: "Execute a query in an external data connector", type: "ACTION", stepId: "EXECUTE_QUERY", + internal: true, inputs: {}, schema: { inputs: { @@ -42,7 +44,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, appId, emitter }) { +exports.run = async function ({ inputs, appId, emitter }) { if (inputs.query == null) { return { success: false, @@ -54,28 +56,22 @@ module.exports.run = async function ({ inputs, appId, emitter }) { const { queryId, ...rest } = inputs.query - const ctx = { + const ctx = buildCtx(appId, emitter, { + body: { + parameters: rest, + }, params: { queryId, }, - request: { - body: { - parameters: rest, - }, - }, - appId, - eventEmitter: emitter, - } - - await queryController.execute(ctx) + }) try { + await queryController.execute(ctx) return { response: ctx.body, - success: ctx.status === 200, + success: true, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/executeScript.js b/packages/server/src/automations/steps/executeScript.js index 33ffd3ee8e..70298b9f8f 100644 --- a/packages/server/src/automations/steps/executeScript.js +++ b/packages/server/src/automations/steps/executeScript.js @@ -1,11 +1,13 @@ const scriptController = require("../../api/controllers/script") +const { buildCtx } = require("./utils") -module.exports.definition = { +exports.definition = { name: "JS Scripting", tagline: "Execute JavaScript Code", icon: "ri-terminal-box-line", description: "Run a piece of JavaScript code in your automation", type: "ACTION", + internal: true, stepId: "EXECUTE_SCRIPT", inputs: {}, schema: { @@ -23,8 +25,7 @@ module.exports.definition = { properties: { value: { type: "string", - description: - "The result of the last statement of the executed script.", + description: "The result of the return statement", }, success: { type: "boolean", @@ -36,7 +37,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, appId, context, emitter }) { +exports.run = async function ({ inputs, appId, context, emitter }) { if (inputs.code == null) { return { success: false, @@ -46,25 +47,20 @@ module.exports.run = async function ({ inputs, appId, context, emitter }) { } } - const ctx = { - request: { - body: { - script: inputs.code, - context, - }, + const ctx = buildCtx(appId, emitter, { + body: { + script: inputs.code, + context, }, - user: { appId }, - eventEmitter: emitter, - } + }) try { await scriptController.execute(ctx) return { - success: ctx.status === 200, + success: true, value: ctx.body, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/filter.js b/packages/server/src/automations/steps/filter.js index 586e424cc4..84bdc10c1d 100644 --- a/packages/server/src/automations/steps/filter.js +++ b/packages/server/src/automations/steps/filter.js @@ -1,29 +1,30 @@ -const LogicConditions = { +const FilterConditions = { 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", +const PrettyFilterConditions = { + [FilterConditions.EQUAL]: "Equals", + [FilterConditions.NOT_EQUAL]: "Not equals", + [FilterConditions.GREATER_THAN]: "Greater than", + [FilterConditions.LESS_THAN]: "Less than", } -module.exports.LogicConditions = LogicConditions -module.exports.PrettyLogicConditions = PrettyLogicConditions +exports.FilterConditions = FilterConditions +exports.PrettyFilterConditions = PrettyFilterConditions -module.exports.definition = { +exports.definition = { name: "Filter", tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}", icon: "ri-git-branch-line", description: "Filter any automations which do not meet certain conditions", type: "LOGIC", + internal: true, stepId: "FILTER", inputs: { - condition: LogicConditions.EQUALS, + condition: FilterConditions.EQUALS, }, schema: { inputs: { @@ -35,8 +36,8 @@ module.exports.definition = { condition: { type: "string", title: "Condition", - enum: Object.values(LogicConditions), - pretty: Object.values(PrettyLogicConditions), + enum: Object.values(FilterConditions), + pretty: Object.values(PrettyFilterConditions), }, value: { type: "string", @@ -57,7 +58,7 @@ module.exports.definition = { }, } -module.exports.run = async function filter({ inputs }) { +exports.run = async function filter({ inputs }) { let { field, condition, value } = inputs // coerce types so that we can use them if (!isNaN(value) && !isNaN(field)) { @@ -70,16 +71,16 @@ module.exports.run = async function filter({ inputs }) { let success = false if (typeof field !== "object" && typeof value !== "object") { switch (condition) { - case LogicConditions.EQUAL: + case FilterConditions.EQUAL: success = field === value break - case LogicConditions.NOT_EQUAL: + case FilterConditions.NOT_EQUAL: success = field !== value break - case LogicConditions.GREATER_THAN: + case FilterConditions.GREATER_THAN: success = field > value break - case LogicConditions.LESS_THAN: + case FilterConditions.LESS_THAN: success = field < value break } diff --git a/packages/server/src/automations/steps/integromat.js b/packages/server/src/automations/steps/integromat.js new file mode 100644 index 0000000000..e7ea03efca --- /dev/null +++ b/packages/server/src/automations/steps/integromat.js @@ -0,0 +1,87 @@ +const fetch = require("node-fetch") +const { getFetchResponse } = require("./utils") + +exports.definition = { + name: "Integromat Integration", + tagline: "Trigger an Integromat scenario", + description: + "Performs a webhook call to Integromat and gets the response (if configured)", + icon: "ri-shut-down-line", + stepId: "integromat", + type: "ACTION", + internal: false, + inputs: {}, + schema: { + inputs: { + properties: { + url: { + type: "string", + title: "Webhook URL", + }, + value1: { + type: "string", + title: "Input Value 1", + }, + value2: { + type: "string", + title: "Input Value 2", + }, + value3: { + type: "string", + title: "Input Value 3", + }, + value4: { + type: "string", + title: "Input Value 4", + }, + value5: { + type: "string", + title: "Input Value 5", + }, + }, + required: ["url", "value1", "value2", "value3", "value4", "value5"], + }, + outputs: { + properties: { + success: { + type: "boolean", + description: "Whether call was successful", + }, + httpStatus: { + type: "number", + description: "The HTTP status code returned", + }, + response: { + type: "object", + description: "The webhook response - this can have properties", + }, + }, + required: ["success", "response"], + }, + }, +} + +exports.run = async function ({ inputs }) { + const { url, value1, value2, value3, value4, value5 } = inputs + + const response = await fetch(url, { + method: "post", + body: JSON.stringify({ + value1, + value2, + value3, + value4, + value5, + }), + headers: { + "Content-Type": "application/json", + }, + }) + + const { status, message } = await getFetchResponse(response) + return { + httpStatus: status, + success: status === 200, + response: message, + } +} diff --git a/packages/server/src/automations/steps/outgoingWebhook.js b/packages/server/src/automations/steps/outgoingWebhook.js index c1edde7b4e..ea0f0ce6be 100644 --- a/packages/server/src/automations/steps/outgoingWebhook.js +++ b/packages/server/src/automations/steps/outgoingWebhook.js @@ -1,4 +1,5 @@ const fetch = require("node-fetch") +const { getFetchResponse } = require("./utils") const RequestType = { POST: "POST", @@ -16,12 +17,13 @@ const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH] * GET/DELETE requests cannot handle body elements so they will not be sent if configured. */ -module.exports.definition = { +exports.definition = { name: "Outgoing webhook", tagline: "Send a {{inputs.requestMethod}} request", icon: "ri-send-plane-line", description: "Send a request of specified method to a URL", type: "ACTION", + internal: true, stepId: "OUTGOING_WEBHOOK", inputs: { requestMethod: "POST", @@ -60,6 +62,10 @@ module.exports.definition = { type: "object", description: "The response from the webhook", }, + httpStatus: { + type: "number", + description: "The HTTP status code returned", + }, success: { type: "boolean", description: "Whether the action was successful", @@ -70,7 +76,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs }) { +exports.run = async function ({ inputs }) { let { requestMethod, url, requestBody, headers } = inputs if (!url.startsWith("http")) { url = `http://${url}` @@ -107,19 +113,11 @@ module.exports.run = async function ({ inputs }) { JSON.parse(request.body) } const response = await fetch(url, request) - const contentType = response.headers.get("content-type") - const success = response.status === 200 - let resp - if (!success) { - resp = response.statusText - } else if (contentType && contentType.indexOf("application/json") !== -1) { - resp = await response.json() - } else { - resp = await response.text() - } + const { status, message } = await getFetchResponse(response) return { - response: resp, - success: success, + httpStatus: status, + response: message, + success: status === 200, } } catch (err) { /* istanbul ignore next */ diff --git a/packages/server/src/automations/steps/sendSmtpEmail.js b/packages/server/src/automations/steps/sendSmtpEmail.js index 764972b402..552c9b4d36 100644 --- a/packages/server/src/automations/steps/sendSmtpEmail.js +++ b/packages/server/src/automations/steps/sendSmtpEmail.js @@ -1,11 +1,12 @@ const { sendSmtpEmail } = require("../../utilities/workerRequests") -module.exports.definition = { +exports.definition = { description: "Send an email using SMTP", tagline: "Send SMTP email to {{inputs.to}}", icon: "ri-mail-open-line", name: "Send Email (SMTP)", type: "ACTION", + internal: true, stepId: "SEND_EMAIL_SMTP", inputs: {}, schema: { @@ -46,7 +47,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs }) { +exports.run = async function ({ inputs }) { let { to, from, subject, contents } = inputs if (!contents) { contents = "

No content

" @@ -58,7 +59,6 @@ module.exports.run = async function ({ inputs }) { response, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/sendgridEmail.js b/packages/server/src/automations/steps/sendgridEmail.js deleted file mode 100644 index 5485116e89..0000000000 --- a/packages/server/src/automations/steps/sendgridEmail.js +++ /dev/null @@ -1,74 +0,0 @@ -module.exports.definition = { - description: "Send an email using SendGrid", - tagline: "Send email to {{inputs.to}}", - icon: "ri-mail-open-line", - name: "Send Email (SendGrid)", - type: "ACTION", - stepId: "SEND_EMAIL", - inputs: {}, - schema: { - inputs: { - properties: { - apiKey: { - type: "string", - title: "SendGrid API key", - }, - to: { - type: "string", - title: "Send To", - }, - from: { - type: "string", - title: "Send From", - }, - subject: { - type: "string", - title: "Email Subject", - }, - contents: { - type: "string", - title: "Email Contents", - }, - }, - required: ["to", "from", "subject", "contents"], - }, - outputs: { - properties: { - success: { - type: "boolean", - description: "Whether the email was sent", - }, - response: { - type: "object", - description: "A response from the email client, this may be an error", - }, - }, - required: ["success"], - }, - }, -} - -module.exports.run = async function ({ inputs }) { - const sgMail = require("@sendgrid/mail") - sgMail.setApiKey(inputs.apiKey) - const msg = { - to: inputs.to, - from: inputs.from, - subject: inputs.subject, - text: inputs.contents ? inputs.contents : "Empty", - } - - try { - let response = await sgMail.send(msg) - return { - success: true, - response, - } - } catch (err) { - console.error(err) - return { - success: false, - response: err, - } - } -} diff --git a/packages/server/src/automations/steps/serverLog.js b/packages/server/src/automations/steps/serverLog.js index 7389b65f54..82e7d073e3 100644 --- a/packages/server/src/automations/steps/serverLog.js +++ b/packages/server/src/automations/steps/serverLog.js @@ -4,12 +4,13 @@ * GET/DELETE requests cannot handle body elements so they will not be sent if configured. */ -module.exports.definition = { +exports.definition = { name: "Backend log", tagline: "Console log a value in the backend", icon: "ri-server-line", description: "Logs the given text to the server (using console.log)", type: "ACTION", + internal: true, stepId: "SERVER_LOG", inputs: { text: "", @@ -36,6 +37,9 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, appId }) { +exports.run = async function ({ inputs, appId }) { console.log(`App ${appId} - ${inputs.text}`) + return { + success: true, + } } diff --git a/packages/server/src/automations/steps/slack.js b/packages/server/src/automations/steps/slack.js new file mode 100644 index 0000000000..ec6341a26f --- /dev/null +++ b/packages/server/src/automations/steps/slack.js @@ -0,0 +1,64 @@ +const fetch = require("node-fetch") +const { getFetchResponse } = require("./utils") + +exports.definition = { + name: "Slack Message", + tagline: "Send a message to Slack", + description: "Send a message to Slack", + icon: "ri-slack-line", + stepId: "slack", + type: "ACTION", + internal: false, + inputs: {}, + schema: { + inputs: { + properties: { + url: { + type: "string", + title: "Incoming Webhook URL", + }, + text: { + type: "string", + title: "Message", + }, + }, + required: ["url", "text"], + }, + outputs: { + properties: { + httpStatus: { + type: "number", + description: "The HTTP status code of the request", + }, + success: { + type: "boolean", + description: "Whether the message sent successfully", + }, + response: { + type: "string", + description: "The response from the Slack Webhook", + }, + }, + }, + }, +} + +exports.run = async function ({ inputs }) { + let { url, text } = inputs + const response = await fetch(url, { + method: "post", + body: JSON.stringify({ + text, + }), + headers: { + "Content-Type": "application/json", + }, + }) + + const { status, message } = await getFetchResponse(response) + return { + httpStatus: status, + response: message, + success: status === 200, + } +} diff --git a/packages/server/src/automations/steps/updateRow.js b/packages/server/src/automations/steps/updateRow.js index 206e429efa..961f75dee7 100644 --- a/packages/server/src/automations/steps/updateRow.js +++ b/packages/server/src/automations/steps/updateRow.js @@ -1,12 +1,13 @@ const rowController = require("../../api/controllers/row") const automationUtils = require("../automationUtils") -module.exports.definition = { +exports.definition = { name: "Update Row", tagline: "Update a {{inputs.enriched.table.name}} row", icon: "ri-refresh-line", description: "Update a row in your database", type: "ACTION", + internal: true, stepId: "UPDATE_ROW", inputs: {}, schema: { @@ -53,7 +54,7 @@ module.exports.definition = { }, } -module.exports.run = async function ({ inputs, appId, emitter }) { +exports.run = async function ({ inputs, appId, emitter }) { if (inputs.rowId == null || inputs.row == null) { return { success: false, @@ -100,7 +101,6 @@ module.exports.run = async function ({ inputs, appId, emitter }) { success: ctx.status === 200, } } catch (err) { - console.error(err) return { success: false, response: err, diff --git a/packages/server/src/automations/steps/utils.js b/packages/server/src/automations/steps/utils.js new file mode 100644 index 0000000000..61f4a8080d --- /dev/null +++ b/packages/server/src/automations/steps/utils.js @@ -0,0 +1,36 @@ +exports.getFetchResponse = async fetched => { + let status = fetched.status, + message + const contentType = fetched.headers.get("content-type") + try { + if (contentType && contentType.indexOf("application/json") !== -1) { + message = await fetched.json() + } else { + message = await fetched.text() + } + } catch (err) { + message = "Failed to retrieve response" + } + return { status, message } +} + +// need to make sure all ctx structures have the +// throw added to them, so that controllers don't +// throw a ctx.throw undefined when error occurs +exports.buildCtx = (appId, emitter, { body, params } = {}) => { + const ctx = { + appId, + user: { appId }, + eventEmitter: emitter, + throw: (code, error) => { + throw error + }, + } + if (body) { + ctx.request = { body } + } + if (params) { + ctx.params = params + } + return ctx +} diff --git a/packages/server/src/automations/steps/zapier.js b/packages/server/src/automations/steps/zapier.js new file mode 100644 index 0000000000..bec90497cd --- /dev/null +++ b/packages/server/src/automations/steps/zapier.js @@ -0,0 +1,84 @@ +const fetch = require("node-fetch") +const { getFetchResponse } = require("./utils") + +exports.definition = { + name: "Zapier Webhook", + stepId: "zapier", + type: "ACTION", + internal: false, + description: "Trigger a Zapier Zap via webhooks", + tagline: "Trigger a Zapier Zap", + icon: "ri-flashlight-line", + schema: { + inputs: { + properties: { + url: { + type: "string", + title: "Webhook URL", + }, + value1: { + type: "string", + title: "Payload Value 1", + }, + value2: { + type: "string", + title: "Payload Value 2", + }, + value3: { + type: "string", + title: "Payload Value 3", + }, + value4: { + type: "string", + title: "Payload Value 4", + }, + value5: { + type: "string", + title: "Payload Value 5", + }, + }, + required: ["url"], + }, + outputs: { + properties: { + httpStatus: { + type: "number", + description: "The HTTP status code of the request", + }, + response: { + type: "string", + description: "The response from Zapier", + }, + }, + }, + }, +} + +exports.run = async function ({ inputs }) { + const { url, value1, value2, value3, value4, value5 } = inputs + + // send the platform to make sure zaps always work, even + // if no values supplied + const response = await fetch(url, { + method: "post", + body: JSON.stringify({ + platform: "budibase", + value1, + value2, + value3, + value4, + value5, + }), + headers: { + "Content-Type": "application/json", + }, + }) + + const { status, message } = await getFetchResponse(response) + + return { + success: status === 200, + httpStatus: status, + response: message, + } +} diff --git a/packages/server/src/automations/tests/automation.spec.js b/packages/server/src/automations/tests/automation.spec.js index 05ec44d70e..e1b82e4327 100644 --- a/packages/server/src/automations/tests/automation.spec.js +++ b/packages/server/src/automations/tests/automation.spec.js @@ -1,34 +1,18 @@ jest.mock("../../utilities/usageQuota") jest.mock("../thread") jest.spyOn(global.console, "error") -jest.mock("worker-farm", () => { - return () => { - const value = jest - .fn() - .mockReturnValueOnce(undefined) - .mockReturnValueOnce("Error") - return (input, callback) => { - workerJob = input - if (callback) { - callback(value()) - } - } - } -}) require("../../environment") const automation = require("../index") const usageQuota = require("../../utilities/usageQuota") const thread = require("../thread") const triggers = require("../triggers") -const { basicAutomation, basicTable } = require("../../tests/utilities/structures") +const { basicAutomation } = require("../../tests/utilities/structures") const { wait } = require("../../utilities") const { makePartial } = require("../../tests/utilities") const { cleanInputValues } = require("../automationUtils") const setup = require("./utilities") -let workerJob - usageQuota.getAPIKey.mockReturnValue({ apiKey: "test" }) describe("Run through some parts of the automations system", () => { @@ -44,59 +28,12 @@ describe("Run through some parts of the automations system", () => { it("should be able to init in builder", async () => { await triggers.externalTrigger(basicAutomation(), { a: 1 }) await wait(100) - expect(workerJob).toBeUndefined() expect(thread).toHaveBeenCalled() }) it("should be able to init in prod", async () => { - await setup.runInProd(async () => { - await triggers.externalTrigger(basicAutomation(), { a: 1 }) - await wait(100) - // haven't added a mock implementation so getAPIKey of usageQuota just returns undefined - expect(usageQuota.update).toHaveBeenCalledWith("test", "automationRuns", 1) - expect(workerJob).toBeDefined() - }) - }) - - it("try error scenario", async () => { - await setup.runInProd(async () => { - // the second call will throw an error - await triggers.externalTrigger(basicAutomation(), { a: 1 }) - await wait(100) - expect(console.error).toHaveBeenCalled() - }) - }) - - it("should be able to check triggering row filling", async () => { - const automation = basicAutomation() - let table = basicTable() - table.schema.boolean = { - type: "boolean", - constraints: { - type: "boolean", - }, - } - table.schema.number = { - type: "number", - constraints: { - type: "number", - }, - } - table.schema.datetime = { - type: "datetime", - constraints: { - type: "datetime", - }, - } - table = await config.createTable(table) - automation.definition.trigger.inputs.tableId = table._id - const params = await triggers.fillRowOutput(automation, { appId: config.getAppId() }) - expect(params.row).toBeDefined() - const date = new Date(params.row.datetime) - expect(typeof params.row.name).toBe("string") - expect(typeof params.row.boolean).toBe("boolean") - expect(typeof params.row.number).toBe("number") - expect(date.getFullYear()).toBe(1970) + await triggers.externalTrigger(basicAutomation(), { a: 1 }) + await wait(100) }) it("should check coercion", async () => { diff --git a/packages/server/src/automations/tests/delay.spec.js b/packages/server/src/automations/tests/delay.spec.js index 99046e8171..d06bd6cf00 100644 --- a/packages/server/src/automations/tests/delay.spec.js +++ b/packages/server/src/automations/tests/delay.spec.js @@ -4,7 +4,7 @@ describe("test the delay logic", () => { it("should be able to run the delay", async () => { const time = 100 const before = Date.now() - await setup.runStep(setup.logic.DELAY.stepId, { time: time }) + await setup.runStep(setup.actions.DELAY.stepId, { time: time }) const now = Date.now() // divide by two just so that test will always pass as long as there was some sort of delay expect(now - before).toBeGreaterThanOrEqual(time / 2) diff --git a/packages/server/src/automations/tests/filter.spec.js b/packages/server/src/automations/tests/filter.spec.js index 05361f43ed..7895433fe9 100644 --- a/packages/server/src/automations/tests/filter.spec.js +++ b/packages/server/src/automations/tests/filter.spec.js @@ -1,48 +1,48 @@ const setup = require("./utilities") -const { LogicConditions } = require("../steps/filter") +const { FilterConditions } = require("../steps/filter") describe("test the filter logic", () => { async function checkFilter(field, condition, value, pass = true) { - let res = await setup.runStep(setup.logic.FILTER.stepId, + let res = await setup.runStep(setup.actions.FILTER.stepId, { field, condition, value } ) expect(res.success).toEqual(pass) } it("should be able test equality", async () => { - await checkFilter("hello", LogicConditions.EQUAL, "hello", true) - await checkFilter("hello", LogicConditions.EQUAL, "no", false) + await checkFilter("hello", FilterConditions.EQUAL, "hello", true) + await checkFilter("hello", FilterConditions.EQUAL, "no", false) }) it("should be able to test greater than", async () => { - await checkFilter(10, LogicConditions.GREATER_THAN, 5, true) - await checkFilter(10, LogicConditions.GREATER_THAN, 15, false) + await checkFilter(10, FilterConditions.GREATER_THAN, 5, true) + await checkFilter(10, FilterConditions.GREATER_THAN, 15, false) }) it("should be able to test less than", async () => { - await checkFilter(5, LogicConditions.LESS_THAN, 10, true) - await checkFilter(15, LogicConditions.LESS_THAN, 10, false) + await checkFilter(5, FilterConditions.LESS_THAN, 10, true) + await checkFilter(15, FilterConditions.LESS_THAN, 10, false) }) it("should be able to in-equality", async () => { - await checkFilter("hello", LogicConditions.NOT_EQUAL, "no", true) - await checkFilter(10, LogicConditions.NOT_EQUAL, 10, false) + await checkFilter("hello", FilterConditions.NOT_EQUAL, "no", true) + await checkFilter(10, FilterConditions.NOT_EQUAL, 10, false) }) it("check number coercion", async () => { - await checkFilter("10", LogicConditions.GREATER_THAN, "5", true) + await checkFilter("10", FilterConditions.GREATER_THAN, "5", true) }) it("check date coercion", async () => { await checkFilter( (new Date()).toISOString(), - LogicConditions.GREATER_THAN, + FilterConditions.GREATER_THAN, (new Date(-10000)).toISOString(), true ) }) it("check objects always false", async () => { - await checkFilter({}, LogicConditions.EQUAL, {}, false) + await checkFilter({}, FilterConditions.EQUAL, {}, false) }) }) \ No newline at end of file diff --git a/packages/server/src/automations/tests/sendEmail.spec.js b/packages/server/src/automations/tests/sendEmail.spec.js deleted file mode 100644 index 5f3aabfff8..0000000000 --- a/packages/server/src/automations/tests/sendEmail.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -const setup = require("./utilities") - -jest.mock("@sendgrid/mail") - -describe("test the send email action", () => { - let inputs - let config = setup.getConfig() - - beforeEach(async () => { - await config.init() - inputs = { - to: "me@test.com", - from: "budibase@test.com", - subject: "Testing", - text: "Email contents", - } - }) - - afterAll(setup.afterAll) - - it("should be able to run the action", async () => { - const res = await setup.runStep(setup.actions.SEND_EMAIL.stepId, inputs) - expect(res.success).toEqual(true) - // the mocked module throws back the input - expect(res.response.to).toEqual("me@test.com") - }) - - it("should return an error if input an invalid email address", async () => { - const res = await setup.runStep(setup.actions.SEND_EMAIL.stepId, { - ...inputs, - to: "invalid@test.com", - }) - expect(res.success).toEqual(false) - }) - -}) diff --git a/packages/server/src/automations/tests/utilities/index.js b/packages/server/src/automations/tests/utilities/index.js index ab9de55430..ca01ea9233 100644 --- a/packages/server/src/automations/tests/utilities/index.js +++ b/packages/server/src/automations/tests/utilities/index.js @@ -1,6 +1,5 @@ const TestConfig = require("../../../tests/utilities/TestConfiguration") const actions = require("../../actions") -const logic = require("../../logic") const emitter = require("../../../events/index") const env = require("../../../environment") @@ -34,16 +33,7 @@ exports.runInProd = async fn => { } exports.runStep = async function runStep(stepId, inputs) { - let step - if ( - Object.values(exports.actions) - .map(action => action.stepId) - .includes(stepId) - ) { - step = await actions.getAction(stepId) - } else { - step = logic.getLogic(stepId) - } + let step = await actions.getAction(stepId) expect(step).toBeDefined() return step({ inputs, @@ -56,5 +46,4 @@ exports.runStep = async function runStep(stepId, inputs) { exports.apiKey = "test" -exports.actions = actions.BUILTIN_DEFINITIONS -exports.logic = logic.BUILTIN_DEFINITIONS +exports.actions = actions.ACTION_DEFINITIONS diff --git a/packages/server/src/automations/thread.js b/packages/server/src/automations/thread.js index aada0ca0ca..a3e81a2274 100644 --- a/packages/server/src/automations/thread.js +++ b/packages/server/src/automations/thread.js @@ -1,5 +1,4 @@ const actions = require("./actions") -const logic = require("./logic") const automationUtils = require("./automationUtils") const AutomationEmitter = require("../events/AutomationEmitter") const { processObject } = require("@budibase/string-templates") @@ -8,7 +7,7 @@ const CouchDB = require("../db") const { DocumentTypes } = require("../db/utils") const { doInTenant } = require("@budibase/auth/tenancy") -const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId +const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId /** * The automation orchestrator is a class responsible for executing automations. @@ -30,15 +29,15 @@ class Orchestrator { // 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) + this.executionOutput = { trigger: {}, steps: [] } + // setup the execution output + const triggerStepId = automation.definition.trigger.stepId + const triggerId = automation.definition.trigger.id + this.updateExecutionOutput(triggerId, triggerStepId, null, triggerOutput) } - async getStepFunctionality(type, stepId) { - let step = null - if (type === "ACTION") { - step = await actions.getAction(stepId) - } else if (type === "LOGIC") { - step = logic.getLogic(stepId) - } + async getStepFunctionality(stepId) { + let step = await actions.getAction(stepId) if (step == null) { throw `Cannot find automation step by name ${stepId}` } @@ -55,11 +54,20 @@ class Orchestrator { return this._app } + updateExecutionOutput(id, stepId, inputs, outputs) { + const stepObj = { id, stepId, inputs, outputs } + // first entry is always the trigger (constructor) + if (this.executionOutput.steps.length === 0) { + this.executionOutput.trigger = stepObj + } + this.executionOutput.steps.push(stepObj) + } + async execute() { let automation = this._automation const app = await this.getApp() for (let step of automation.definition.steps) { - let stepFn = await this.getStepFunctionality(step.type, step.stepId) + let stepFn = await this.getStepFunctionality(step.stepId) step.inputs = await processObject(step.inputs, this._context) step.inputs = automationUtils.cleanInputValues( step.inputs, @@ -81,27 +89,20 @@ class Orchestrator { break } this._context.steps.push(outputs) + this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs) } catch (err) { console.error(`Automation error - ${step.stepId} - ${err}`) + return err } } + return this.executionOutput } } -// callback is required for worker-farm to state that the worker thread has completed -module.exports = async (job, cb = null) => { - try { - const automationOrchestrator = new Orchestrator( - job.data.automation, - job.data.event - ) - await automationOrchestrator.execute() - if (cb) { - cb() - } - } catch (err) { - if (cb) { - cb(err) - } - } +module.exports = async job => { + const automationOrchestrator = new Orchestrator( + job.data.automation, + job.data.event + ) + return automationOrchestrator.execute() } diff --git a/packages/server/src/automations/triggerInfo/app.js b/packages/server/src/automations/triggerInfo/app.js new file mode 100644 index 0000000000..1c64795cf3 --- /dev/null +++ b/packages/server/src/automations/triggerInfo/app.js @@ -0,0 +1,31 @@ +exports.definition = { + name: "App Action", + event: "app:trigger", + icon: "ri-window-fill", + tagline: "Automation fired from the frontend", + description: "Trigger an automation from an action inside your app", + stepId: "APP", + inputs: {}, + schema: { + inputs: { + properties: { + fields: { + type: "object", + customType: "triggerSchema", + title: "Fields", + }, + }, + required: [], + }, + outputs: { + properties: { + fields: { + type: "object", + description: "Fields submitted from the app frontend", + }, + }, + required: ["fields"], + }, + }, + type: "TRIGGER", +} diff --git a/packages/server/src/automations/triggerInfo/cron.js b/packages/server/src/automations/triggerInfo/cron.js new file mode 100644 index 0000000000..31e79a4abf --- /dev/null +++ b/packages/server/src/automations/triggerInfo/cron.js @@ -0,0 +1,31 @@ +exports.definition = { + name: "Cron Trigger", + event: "cron:trigger", + icon: "ri-timer-line", + tagline: "Cron Trigger ({{inputs.cron}})", + description: "Triggers automation on a cron schedule.", + stepId: "CRON", + inputs: {}, + schema: { + inputs: { + properties: { + cron: { + type: "string", + customType: "cron", + title: "Expression", + }, + }, + required: ["cron"], + }, + outputs: { + properties: { + timestamp: { + type: "number", + description: "Timestamp the cron was executed", + }, + }, + required: ["timestamp"], + }, + }, + type: "TRIGGER", +} diff --git a/packages/server/src/automations/triggerInfo/index.js b/packages/server/src/automations/triggerInfo/index.js new file mode 100644 index 0000000000..066993d324 --- /dev/null +++ b/packages/server/src/automations/triggerInfo/index.js @@ -0,0 +1,15 @@ +const app = require("./app") +const cron = require("./cron") +const rowDeleted = require("./rowDeleted") +const rowSaved = require("./rowSaved") +const rowUpdated = require("./rowUpdated") +const webhook = require("./webhook") + +exports.definitions = { + ROW_SAVED: rowSaved.definition, + ROW_UPDATED: rowUpdated.definition, + ROW_DELETED: rowDeleted.definition, + WEBHOOK: webhook.definition, + APP: app.definition, + CRON: cron.definition, +} diff --git a/packages/server/src/automations/triggerInfo/rowDeleted.js b/packages/server/src/automations/triggerInfo/rowDeleted.js new file mode 100644 index 0000000000..c7ead1fec4 --- /dev/null +++ b/packages/server/src/automations/triggerInfo/rowDeleted.js @@ -0,0 +1,32 @@ +exports.definition = { + name: "Row Deleted", + event: "row:delete", + icon: "ri-delete-bin-line", + tagline: "Row is deleted from {{inputs.enriched.table.name}}", + description: "Fired when a row is deleted from your database", + stepId: "ROW_DELETED", + inputs: {}, + schema: { + inputs: { + properties: { + tableId: { + type: "string", + customType: "table", + title: "Table", + }, + }, + required: ["tableId"], + }, + outputs: { + properties: { + row: { + type: "object", + customType: "row", + description: "The row that was deleted", + }, + }, + required: ["row"], + }, + }, + type: "TRIGGER", +} diff --git a/packages/server/src/automations/triggerInfo/rowSaved.js b/packages/server/src/automations/triggerInfo/rowSaved.js new file mode 100644 index 0000000000..3a21a26878 --- /dev/null +++ b/packages/server/src/automations/triggerInfo/rowSaved.js @@ -0,0 +1,40 @@ +exports.definition = { + name: "Row Created", + event: "row:save", + icon: "ri-save-line", + tagline: "Row is added to {{inputs.enriched.table.name}}", + description: "Fired when a row is added to your database", + stepId: "ROW_SAVED", + inputs: {}, + schema: { + inputs: { + properties: { + tableId: { + type: "string", + customType: "table", + title: "Table", + }, + }, + required: ["tableId"], + }, + outputs: { + properties: { + row: { + type: "object", + customType: "row", + description: "The new row that was created", + }, + id: { + type: "string", + description: "Row ID - can be used for updating", + }, + revision: { + type: "string", + description: "Revision of row", + }, + }, + required: ["row", "id"], + }, + }, + type: "TRIGGER", +} diff --git a/packages/server/src/automations/triggerInfo/rowUpdated.js b/packages/server/src/automations/triggerInfo/rowUpdated.js new file mode 100644 index 0000000000..099ce0a6b2 --- /dev/null +++ b/packages/server/src/automations/triggerInfo/rowUpdated.js @@ -0,0 +1,40 @@ +exports.definition = { + name: "Row Updated", + event: "row:update", + icon: "ri-refresh-line", + tagline: "Row is updated in {{inputs.enriched.table.name}}", + description: "Fired when a row is updated in your database", + stepId: "ROW_UPDATED", + inputs: {}, + schema: { + inputs: { + properties: { + tableId: { + type: "string", + customType: "table", + title: "Table", + }, + }, + required: ["tableId"], + }, + outputs: { + properties: { + row: { + type: "object", + customType: "row", + description: "The row that was updated", + }, + id: { + type: "string", + description: "Row ID - can be used for updating", + }, + revision: { + type: "string", + description: "Revision of row", + }, + }, + required: ["row", "id"], + }, + }, + type: "TRIGGER", +} diff --git a/packages/server/src/automations/triggerInfo/webhook.js b/packages/server/src/automations/triggerInfo/webhook.js new file mode 100644 index 0000000000..dd83031d8f --- /dev/null +++ b/packages/server/src/automations/triggerInfo/webhook.js @@ -0,0 +1,36 @@ +exports.definition = { + name: "Webhook", + event: "web:trigger", + icon: "ri-global-line", + tagline: "Webhook endpoint is hit", + description: "Trigger an automation when a HTTP POST webhook is hit", + stepId: "WEBHOOK", + inputs: {}, + schema: { + inputs: { + properties: { + schemaUrl: { + type: "string", + customType: "webhookUrl", + title: "Schema URL", + }, + triggerUrl: { + type: "string", + customType: "webhookUrl", + title: "Trigger URL", + }, + }, + required: ["schemaUrl", "triggerUrl"], + }, + outputs: { + properties: { + body: { + type: "object", + description: "Body of the request which hit the webhook", + }, + }, + required: ["body"], + }, + }, + type: "TRIGGER", +} diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index 2a2d68ecb1..ae98f0e73a 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -1,244 +1,22 @@ const CouchDB = require("../db") const emitter = require("../events/index") -const env = require("../environment") -const Queue = env.isTest() - ? require("../utilities/queue/inMemoryQueue") - : require("bull") const { getAutomationParams } = require("../db/utils") const { coerce } = require("../utilities/rowProcessor") -const { utils } = require("@budibase/auth/redis") -const { JobQueues } = require("../constants") -const { - isExternalTable, - breakExternalTableId, -} = require("../integrations/utils") -const { getExternalTable } = require("../api/controllers/table/utils") +const { definitions } = require("./triggerInfo") +const { isDevAppID } = require("../db/utils") +// need this to call directly, so we can get a response +const { queue } = require("./bullboard") +const { checkTestFlag } = require("../utilities/redis") +const utils = require("./utils") +const env = require("../environment") -const { opts } = utils.getRedisOptions() -let automationQueue = new Queue(JobQueues.AUTOMATIONS, { redis: opts }) - -const FAKE_STRING = "TEST" -const FAKE_BOOL = false -const FAKE_NUMBER = 1 -const FAKE_DATETIME = "1970-01-01T00:00:00.000Z" - -const BUILTIN_DEFINITIONS = { - ROW_SAVED: { - name: "Row Created", - event: "row:save", - icon: "ri-save-line", - tagline: "Row is added to {{inputs.enriched.table.name}}", - description: "Fired when a row is added to your database", - stepId: "ROW_SAVED", - inputs: {}, - schema: { - inputs: { - properties: { - tableId: { - type: "string", - customType: "table", - title: "Table", - }, - }, - required: ["tableId"], - }, - outputs: { - properties: { - row: { - type: "object", - customType: "row", - description: "The new row that was created", - }, - id: { - type: "string", - description: "Row ID - can be used for updating", - }, - revision: { - type: "string", - description: "Revision of row", - }, - }, - required: ["row", "id"], - }, - }, - type: "TRIGGER", - }, - ROW_UPDATED: { - name: "Row Updated", - event: "row:update", - icon: "ri-refresh-line", - tagline: "Row is updated in {{inputs.enriched.table.name}}", - description: "Fired when a row is updated in your database", - stepId: "ROW_UPDATED", - inputs: {}, - schema: { - inputs: { - properties: { - tableId: { - type: "string", - customType: "table", - title: "Table", - }, - }, - required: ["tableId"], - }, - outputs: { - properties: { - row: { - type: "object", - customType: "row", - description: "The row that was updated", - }, - id: { - type: "string", - description: "Row ID - can be used for updating", - }, - revision: { - type: "string", - description: "Revision of row", - }, - }, - required: ["row", "id"], - }, - }, - type: "TRIGGER", - }, - ROW_DELETED: { - name: "Row Deleted", - event: "row:delete", - icon: "ri-delete-bin-line", - tagline: "Row is deleted from {{inputs.enriched.table.name}}", - description: "Fired when a row is deleted from your database", - stepId: "ROW_DELETED", - inputs: {}, - schema: { - inputs: { - properties: { - tableId: { - type: "string", - customType: "table", - title: "Table", - }, - }, - required: ["tableId"], - }, - outputs: { - properties: { - row: { - type: "object", - customType: "row", - description: "The row that was deleted", - }, - }, - required: ["row"], - }, - }, - type: "TRIGGER", - }, - WEBHOOK: { - name: "Webhook", - event: "web:trigger", - icon: "ri-global-line", - tagline: "Webhook endpoint is hit", - description: "Trigger an automation when a HTTP POST webhook is hit", - stepId: "WEBHOOK", - inputs: {}, - schema: { - inputs: { - properties: { - schemaUrl: { - type: "string", - customType: "webhookUrl", - title: "Schema URL", - }, - triggerUrl: { - type: "string", - customType: "webhookUrl", - title: "Trigger URL", - }, - }, - required: ["schemaUrl", "triggerUrl"], - }, - outputs: { - properties: { - body: { - type: "object", - description: "Body of the request which hit the webhook", - }, - }, - required: ["body"], - }, - }, - type: "TRIGGER", - }, - APP: { - name: "App Action", - event: "app:trigger", - icon: "ri-window-fill", - tagline: "Automation fired from the frontend", - description: "Trigger an automation from an action inside your app", - stepId: "APP", - inputs: {}, - schema: { - inputs: { - properties: { - fields: { - type: "object", - customType: "triggerSchema", - title: "Fields", - }, - }, - required: [], - }, - outputs: { - properties: { - fields: { - type: "object", - description: "Fields submitted from the app frontend", - }, - }, - required: ["fields"], - }, - }, - type: "TRIGGER", - }, - CRON: { - name: "Cron Trigger", - event: "cron:trigger", - icon: "ri-timer-line", - tagline: "Cron Trigger ({{inputs.cron}})", - description: "Triggers automation on a cron schedule.", - stepId: "CRON", - inputs: {}, - schema: { - inputs: { - properties: { - cron: { - type: "string", - customType: "cron", - title: "Expression", - }, - }, - required: ["cron"], - }, - outputs: { - properties: { - timestamp: { - type: "number", - description: "Timestamp the cron was executed", - }, - }, - required: ["timestamp"], - }, - }, - type: "TRIGGER", - }, -} +const TRIGGER_DEFINITIONS = definitions async function queueRelevantRowAutomations(event, eventType) { if (event.appId == null) { throw `No appId specified for ${eventType} - check event emitters.` } + const db = new CouchDB(event.appId) let automations = await db.allDocs( getAutomationParams(null, { include_docs: true }) @@ -255,14 +33,22 @@ async function queueRelevantRowAutomations(event, eventType) { for (let automation of automations) { let automationDef = automation.definition let automationTrigger = automationDef ? automationDef.trigger : {} + // don't queue events which are for dev apps, only way to test automations is + // running tests on them, in production the test flag will never + // be checked due to lazy evaluation (first always false) if ( - !automation.live || - !automationTrigger.inputs || - automationTrigger.inputs.tableId !== event.row.tableId + !env.ALLOW_DEV_AUTOMATIONS && + isDevAppID(event.appId) && + !(await checkTestFlag(automation._id)) ) { continue } - automationQueue.add({ automation, event }) + if ( + automationTrigger.inputs && + automationTrigger.inputs.tableId === event.row.tableId + ) { + await queue.add({ automation, event }) + } } } @@ -290,51 +76,12 @@ emitter.on("row:delete", async function (event) { await queueRelevantRowAutomations(event, "row:delete") }) -async function fillRowOutput(automation, params) { - let triggerSchema = automation.definition.trigger - let tableId = triggerSchema.inputs.tableId - try { - let table - if (!isExternalTable(tableId)) { - const db = new CouchDB(params.appId) - table = await db.get(tableId) - } else { - const { datasourceId, tableName } = breakExternalTableId(tableId) - table = await getExternalTable(params.appId, datasourceId, tableName) - } - let row = {} - for (let schemaKey of Object.keys(table.schema)) { - const paramValue = params[schemaKey] - let propSchema = table.schema[schemaKey] - switch (propSchema.constraints.type) { - case "string": - row[schemaKey] = paramValue || FAKE_STRING - break - case "boolean": - row[schemaKey] = paramValue || FAKE_BOOL - break - case "number": - row[schemaKey] = paramValue || FAKE_NUMBER - break - case "datetime": - row[schemaKey] = paramValue || FAKE_DATETIME - break - } - } - params.row = row - } catch (err) { - /* istanbul ignore next */ - throw "Failed to find table for trigger" - } - return params -} - -module.exports.externalTrigger = async function (automation, params) { - // TODO: replace this with allowing user in builder to input values in future +exports.externalTrigger = async function ( + automation, + params, + { getResponses } = {} +) { if (automation.definition != null && automation.definition.trigger != null) { - if (automation.definition.trigger.inputs.tableId != null) { - params = await fillRowOutput(automation, params) - } if (automation.definition.trigger.stepId === "APP") { // values are likely to be submitted as strings, so we shall convert to correct type const coercedFields = {} @@ -345,14 +92,12 @@ module.exports.externalTrigger = async function (automation, params) { params.fields = coercedFields } } - - automationQueue.add({ automation, event: params }) + const data = { automation, event: params } + if (getResponses) { + return utils.processEvent({ data }) + } else { + return queue.add(data) + } } -module.exports.getQueues = () => { - return [automationQueue] -} -module.exports.fillRowOutput = fillRowOutput -module.exports.automationQueue = automationQueue - -module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS +exports.TRIGGER_DEFINITIONS = TRIGGER_DEFINITIONS diff --git a/packages/server/src/automations/utils.js b/packages/server/src/automations/utils.js new file mode 100644 index 0000000000..4bef91a153 --- /dev/null +++ b/packages/server/src/automations/utils.js @@ -0,0 +1,159 @@ +const runner = require("./thread") +const { definitions } = require("./triggerInfo") +const webhooks = require("../api/controllers/webhook") +const CouchDB = require("../db") +const { queue } = require("./bullboard") +const newid = require("../db/newid") +const { updateEntityMetadata } = require("../utilities") +const { MetadataTypes } = require("../constants") + +const WH_STEP_ID = definitions.WEBHOOK.stepId +const CRON_STEP_ID = definitions.CRON.stepId + +exports.processEvent = async job => { + try { + // need to actually await these so that an error can be captured properly + return await runner(job) + } catch (err) { + console.error( + `${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}` + ) + return { err } + } +} + +exports.updateTestHistory = async (appId, automation, history) => { + return updateEntityMetadata( + appId, + MetadataTypes.AUTOMATION_TEST_HISTORY, + automation._id, + metadata => { + if (metadata && Array.isArray(metadata.history)) { + metadata.history.push(history) + } else { + metadata = { + history: [history], + } + } + return metadata + } + ) +} + +// end the repetition and the job itself +exports.disableAllCrons = async appId => { + const promises = [] + const jobs = await queue.getRepeatableJobs() + for (let job of jobs) { + if (job.key.includes(`${appId}_cron`)) { + promises.push(queue.removeRepeatableByKey(job.key)) + promises.push(queue.removeJobs(job.id)) + } + } + return Promise.all(promises) +} + +/** + * This function handles checking of any cron jobs that need to be enabled/updated. + * @param {string} appId The ID of the app in which we are checking for webhooks + * @param {object|undefined} automation The automation object to be updated. + */ +exports.enableCronTrigger = async (appId, automation) => { + const trigger = automation ? automation.definition.trigger : null + function isCronTrigger(auto) { + return ( + auto && + auto.definition.trigger && + auto.definition.trigger.stepId === CRON_STEP_ID + ) + } + // need to create cron job + if (isCronTrigger(automation)) { + // make a job id rather than letting Bull decide, makes it easier to handle on way out + const jobId = `${appId}_cron_${newid()}` + const job = await queue.add( + { + automation, + event: { appId, timestamp: Date.now() }, + }, + { repeat: { cron: trigger.inputs.cron }, jobId } + ) + // Assign cron job ID from bull so we can remove it later if the cron trigger is removed + trigger.cronJobId = job.id + const db = new CouchDB(appId) + const response = await db.put(automation) + automation._id = response.id + automation._rev = response.rev + } + return automation +} + +/** + * This function handles checking if any webhooks need to be created or deleted for automations. + * @param {string} appId The ID of the app in which we are checking for webhooks + * @param {object|undefined} oldAuto The old automation object if updating/deleting + * @param {object|undefined} newAuto The new automation object if creating/updating + * @returns {Promise} After this is complete the new automation object may have been updated and should be + * written to DB (this does not write to DB as it would be wasteful to repeat). + */ +exports.checkForWebhooks = async ({ appId, oldAuto, newAuto }) => { + const oldTrigger = oldAuto ? oldAuto.definition.trigger : null + const newTrigger = newAuto ? newAuto.definition.trigger : null + const triggerChanged = + oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id + function isWebhookTrigger(auto) { + return ( + auto && + auto.definition.trigger && + auto.definition.trigger.stepId === WH_STEP_ID + ) + } + // need to delete webhook + if ( + isWebhookTrigger(oldAuto) && + (!isWebhookTrigger(newAuto) || triggerChanged) && + oldTrigger.webhookId + ) { + try { + let db = new CouchDB(appId) + // need to get the webhook to get the rev + const webhook = await db.get(oldTrigger.webhookId) + const ctx = { + appId, + params: { id: webhook._id, rev: webhook._rev }, + } + // might be updating - reset the inputs to remove the URLs + if (newTrigger) { + delete newTrigger.webhookId + newTrigger.inputs = {} + } + await webhooks.destroy(ctx) + } catch (err) { + // don't worry about not being able to delete, if it doesn't exist all good + } + } + // need to create webhook + if ( + (!isWebhookTrigger(oldAuto) || triggerChanged) && + isWebhookTrigger(newAuto) + ) { + const ctx = { + appId, + request: { + body: new webhooks.Webhook( + "Automation webhook", + webhooks.WebhookType.AUTOMATION, + newAuto._id + ), + }, + } + await webhooks.save(ctx) + const id = ctx.body.webhook._id + newTrigger.webhookId = id + newTrigger.inputs = { + schemaUrl: `api/webhooks/schema/${appId}/${id}`, + triggerUrl: `api/webhooks/trigger/${appId}/${id}`, + } + } + return newAuto +} diff --git a/packages/server/src/constants/index.js b/packages/server/src/constants/index.js index bc7b5b368f..bea58fd260 100644 --- a/packages/server/src/constants/index.js +++ b/packages/server/src/constants/index.js @@ -123,5 +123,10 @@ exports.BaseQueryVerbs = { DELETE: "delete", } +exports.MetadataTypes = { + AUTOMATION_TEST_INPUT: "automationTestInput", + AUTOMATION_TEST_HISTORY: "automationTestHistory", +} + // pass through the list from the auth/core lib exports.ObjectStoreBuckets = ObjectStoreBuckets diff --git a/packages/server/src/db/linkedRows/index.js b/packages/server/src/db/linkedRows/index.js index 34be44336c..67412e7e89 100644 --- a/packages/server/src/db/linkedRows/index.js +++ b/packages/server/src/db/linkedRows/index.js @@ -36,6 +36,18 @@ exports.IncludeDocs = IncludeDocs exports.getLinkDocuments = getLinkDocuments exports.createLinkView = createLinkView +function clearRelationshipFields(table, rows) { + for (let [key, field] of Object.entries(table.schema)) { + if (field.type === FieldTypes.LINK) { + rows = rows.map(row => { + delete row[key] + return row + }) + } + } + return rows +} + async function getLinksForRows(appId, rows) { const tableIds = [...new Set(rows.map(el => el.tableId))] // start by getting all the link values for performance reasons @@ -126,33 +138,6 @@ exports.updateLinks = async function (args) { } } -/** - * Update a row with information about the links that pertain to it. - * @param {string} appId The instance in which this row has been created. - * @param {object} rows The row(s) themselves which is to be updated with info (if applicable). This can be - * a single row object or an array of rows - both will be handled. - * @returns {Promise} The updated row (this may be the same if no links were found). If an array was input - * then an array will be output, object input -> object output. - */ -exports.attachLinkIDs = async (appId, rows) => { - const links = await getLinksForRows(appId, rows) - // now iterate through the rows and all field information - for (let row of rows) { - // find anything that matches the row's ID we are searching for and join it - links - .filter(el => el.thisId === row._id) - .forEach(link => { - if (row[link.fieldName] == null) { - row[link.fieldName] = [] - } - row[link.fieldName].push(link.id) - }) - } - // if it was an array when it came in then handle it as an array in response - // otherwise return the first element as there was only one input - return rows -} - /** * Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row. * This is required for formula fields, this may only be utilised internally (for now). @@ -173,6 +158,9 @@ exports.attachFullLinkedDocs = async (ctx, table, rows) => { const links = (await getLinksForRows(appId, rows)).filter(link => rows.some(row => row._id === link.thisId) ) + // clear any existing links that could be dupe'd + rows = clearRelationshipFields(table, rows) + // now get the docs and combine into the rows let linked = await getFullLinkedDocs(ctx, appId, links) const linkedTables = [] for (let row of rows) { diff --git a/packages/server/src/db/tests/linkTests.spec.js b/packages/server/src/db/tests/linkTests.spec.js index 3fed6938b7..8dad7be049 100644 --- a/packages/server/src/db/tests/linkTests.spec.js +++ b/packages/server/src/db/tests/linkTests.spec.js @@ -59,16 +59,4 @@ describe("test link functionality", () => { expect(Array.isArray(output)).toBe(true) }) }) - - describe("attachLinkIDs", () => { - it("should be able to attach linkIDs", async () => { - await config.init() - await config.createTable() - const table = await config.createLinkedTable() - const row = await config.createRow() - const linkRow = await config.createRow(basicLinkedRow(table._id, row._id)) - const attached = await links.attachLinkIDs(config.getAppId(), [linkRow]) - expect(attached[0].link[0]).toBe(row._id) - }) - }) }) \ No newline at end of file diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 706d5ed207..ec1c267fa2 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -7,6 +7,8 @@ const { APP_PREFIX, SEPARATOR, StaticDatabases, + isDevAppID, + isProdAppID, } = require("@budibase/auth/db") const UNICODE_MAX = "\ufff0" @@ -36,6 +38,7 @@ const DocumentTypes = { DATASOURCE_PLUS: "datasource_plus", QUERY: "query", DEPLOYMENTS: "deployments", + METADATA: "metadata", } const ViewNames = { @@ -63,6 +66,8 @@ const BudibaseInternalDB = { exports.APP_PREFIX = APP_PREFIX exports.APP_DEV_PREFIX = APP_DEV_PREFIX +exports.isDevAppID = isDevAppID +exports.isProdAppID = isProdAppID exports.USER_METDATA_PREFIX = `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` exports.LINK_USER_METADATA_PREFIX = `${DocumentTypes.LINK}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` exports.ViewNames = ViewNames @@ -331,6 +336,18 @@ exports.getQueryParams = (datasourceId = null, otherProps = {}) => { ) } +exports.generateMetadataID = (type, entityId) => { + return `${DocumentTypes.METADATA}${SEPARATOR}${type}${SEPARATOR}${entityId}` +} + +exports.getMetadataParams = (type, entityId = null, otherProps = {}) => { + let docId = `${type}${SEPARATOR}` + if (entityId != null) { + docId += entityId + } + return getDocParams(DocumentTypes.METADATA, docId, otherProps) +} + /** * This can be used with the db.allDocs to get a list of IDs */ diff --git a/packages/server/src/environment.js b/packages/server/src/environment.js index 9f69664ffb..9e029e440a 100644 --- a/packages/server/src/environment.js +++ b/packages/server/src/environment.js @@ -55,6 +55,7 @@ module.exports = { BUDIBASE_API_KEY: process.env.BUDIBASE_API_KEY, USERID_API_KEY: process.env.USERID_API_KEY, DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL, + ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS, _set(key, value) { process.env[key] = value module.exports[key] = value diff --git a/packages/server/src/middleware/appInfo.js b/packages/server/src/middleware/appInfo.js new file mode 100644 index 0000000000..ee3655c6cc --- /dev/null +++ b/packages/server/src/middleware/appInfo.js @@ -0,0 +1,19 @@ +const { isDevAppID, isProdAppID } = require("../db/utils") + +exports.AppType = { + DEV: "dev", + PROD: "prod", +} + +exports.middleware = + ({ appType } = {}) => + (ctx, next) => { + const appId = ctx.appId + if (appType === exports.AppType.DEV && appId && !isDevAppID(appId)) { + ctx.throw(400, "Only apps in development support this endpoint") + } + if (appType === exports.AppType.PROD && appId && !isProdAppID(appId)) { + ctx.throw(400, "Only apps in production support this endpoint") + } + return next() + } diff --git a/packages/server/src/middleware/usageQuota.js b/packages/server/src/middleware/usageQuota.js index ac8336e769..4647878721 100644 --- a/packages/server/src/middleware/usageQuota.js +++ b/packages/server/src/middleware/usageQuota.js @@ -14,7 +14,7 @@ const DOMAIN_MAP = { views: usageQuota.Properties.VIEW, users: usageQuota.Properties.USER, // this will not be updated by endpoint calls - // instead it will be updated by triggers + // instead it will be updated by triggerInfo automationRuns: usageQuota.Properties.AUTOMATION, } diff --git a/packages/server/src/utilities/fileSystem/index.js b/packages/server/src/utilities/fileSystem/index.js index b19d7551e4..5226fd66ca 100644 --- a/packages/server/src/utilities/fileSystem/index.js +++ b/packages/server/src/utilities/fileSystem/index.js @@ -14,18 +14,12 @@ const { downloadTarball, } = require("./utilities") const { updateClientLibrary } = require("./clientLibrary") -const download = require("download") const env = require("../../environment") -const { homedir } = require("os") -const fetch = require("node-fetch") const { USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX, } = require("../../db/utils") -const DEFAULT_AUTOMATION_BUCKET = - "https://prod-budi-automations.s3-eu-west-1.amazonaws.com" -const DEFAULT_AUTOMATION_DIRECTORY = ".budibase-automations" const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..") const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules") @@ -205,30 +199,6 @@ exports.getComponentLibraryManifest = async (appId, library) => { return JSON.parse(resp) } -exports.automationInit = async () => { - const directory = - env.AUTOMATION_DIRECTORY || join(homedir(), DEFAULT_AUTOMATION_DIRECTORY) - const bucket = env.AUTOMATION_BUCKET || DEFAULT_AUTOMATION_BUCKET - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }) - } - // env setup to get async packages - let response = await fetch(`${bucket}/manifest.json`) - return response.json() -} - -exports.getExternalAutomationStep = async (name, version, bundleName) => { - const directory = - env.AUTOMATION_DIRECTORY || join(homedir(), DEFAULT_AUTOMATION_DIRECTORY) - const bucket = env.AUTOMATION_BUCKET || DEFAULT_AUTOMATION_BUCKET - try { - return require(join(directory, bundleName)) - } catch (err) { - await download(`${bucket}/${name}/${version}/${bundleName}`, directory) - return require(join(directory, bundleName)) - } -} - /** * All file reads come through here just to make sure all of them make sense * allows a centralised location to check logic is all good. diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index dec17ab284..a81f9ddcf5 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -1,6 +1,8 @@ const env = require("../environment") const { OBJ_STORE_DIRECTORY } = require("../constants") const { sanitizeKey } = require("@budibase/auth/src/objectStore") +const CouchDB = require("../db") +const { generateMetadataID } = require("../db/utils") const BB_CDN = "https://cdn.budi.live" @@ -55,3 +57,52 @@ exports.attachmentsRelativeURL = attachmentKey => { `${exports.objectStoreUrl()}/${attachmentKey}` ) } + +exports.updateEntityMetadata = async (appId, type, entityId, updateFn) => { + const db = new CouchDB(appId) + const id = generateMetadataID(type, entityId) + // read it to see if it exists, we'll overwrite it no matter what + let rev, + metadata = {} + try { + const oldMetadata = await db.get(id) + rev = oldMetadata._rev + metadata = updateFn(oldMetadata) + } catch (err) { + rev = null + metadata = updateFn({}) + } + metadata._id = id + if (rev) { + metadata._rev = rev + } + const response = await db.put(metadata) + return { + ...metadata, + _id: id, + _rev: response.rev, + } +} + +exports.saveEntityMetadata = async (appId, type, entityId, metadata) => { + return exports.updateEntityMetadata(appId, type, entityId, () => { + return metadata + }) +} + +exports.deleteEntityMetadata = async (appId, type, entityId) => { + const db = new CouchDB(appId) + const id = generateMetadataID(type, entityId) + let rev + try { + const metadata = await db.get(id) + if (metadata) { + rev = metadata._rev + } + } catch (err) { + // don't need to error if it doesn't exist + } + if (id && rev) { + await db.remove(id, rev) + } +} diff --git a/packages/server/src/utilities/redis.js b/packages/server/src/utilities/redis.js index 6f1cf46606..151feabdbb 100644 --- a/packages/server/src/utilities/redis.js +++ b/packages/server/src/utilities/redis.js @@ -2,20 +2,24 @@ const { Client, utils } = require("@budibase/auth/redis") const { getGlobalIDFromUserMetadataID } = require("../db/utils") const APP_DEV_LOCK_SECONDS = 600 -let devAppClient, debounceClient +const AUTOMATION_TEST_FLAG_SECONDS = 60 +let devAppClient, debounceClient, flagClient // we init this as we want to keep the connection open all the time // reduces the performance hit exports.init = async () => { devAppClient = new Client(utils.Databases.DEV_LOCKS) debounceClient = new Client(utils.Databases.DEBOUNCE) + flagClient = new Client(utils.Databases.FLAGS) await devAppClient.init() await debounceClient.init() + await flagClient.init() } exports.shutdown = async () => { if (devAppClient) await devAppClient.finish() if (debounceClient) await debounceClient.finish() + if (flagClient) await flagClient.finish() } exports.doesUserHaveLock = async (devAppId, user) => { @@ -67,3 +71,16 @@ exports.checkDebounce = async id => { exports.setDebounce = async (id, seconds) => { await debounceClient.store(id, "debouncing", seconds) } + +exports.setTestFlag = async id => { + await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS) +} + +exports.checkTestFlag = async id => { + const flag = await flagClient.get(id) + return !!(flag && flag.testing) +} + +exports.clearTestFlag = async id => { + await devAppClient.delete(id) +} diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js index 7cb3ac7e02..bb4ac98bb7 100644 --- a/packages/server/src/utilities/rowProcessor/index.js +++ b/packages/server/src/utilities/rowProcessor/index.js @@ -185,10 +185,16 @@ exports.inputProcessing = (user = {}, table, row) => { * @param {object} ctx the request which is looking for enriched rows. * @param {object} table the table from which these rows came from originally, this is used to determine * the schema of the rows and then enrich. - * @param {object[]} rows the rows which are to be enriched. - * @returns {object[]} the enriched rows will be returned. + * @param {object[]|object} rows the rows which are to be enriched. + * @param {object} opts used to set some options for the output, such as disabling relationship squashing. + * @returns {object[]|object} the enriched rows will be returned. */ -exports.outputProcessing = async (ctx, table, rows) => { +exports.outputProcessing = async ( + ctx, + table, + rows, + opts = { squash: true } +) => { const appId = ctx.appId let wasArray = true if (!(rows instanceof Array)) { @@ -214,6 +220,12 @@ exports.outputProcessing = async (ctx, table, rows) => { } } } - enriched = await linkRows.squashLinksToPrimaryDisplay(appId, table, enriched) + if (opts.squash) { + enriched = await linkRows.squashLinksToPrimaryDisplay( + appId, + table, + enriched + ) + } return wasArray ? enriched : enriched[0] } diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index a1fd861be2..a5c6bfeffd 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/worker/package.json b/packages/worker/package.json index 23a65e36bd..552a8dc67a 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "0.9.125-alpha.2", + "version": "0.9.125-alpha.5", "description": "Budibase background service", "main": "src/index.js", "repository": { @@ -23,8 +23,8 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.125-alpha.2", - "@budibase/string-templates": "^0.9.125-alpha.2", + "@budibase/auth": "^0.9.125-alpha.5", + "@budibase/string-templates": "^0.9.125-alpha.5", "@koa/router": "^8.0.0", "@techpass/passport-openidconnect": "^0.3.0", "aws-sdk": "^2.811.0", diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index 24b00fe3a6..7fd367964c 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -33,7 +33,7 @@ async function allUsers() { return response.rows.map(row => row.doc) } -async function saveUser(user, tenantId) { +async function saveUser(user, tenantId, hashPassword = true) { if (!tenantId) { throw "No tenancy specified." } @@ -56,7 +56,7 @@ async function saveUser(user, tenantId) { // get the password, make sure one is defined let hashedPassword if (password) { - hashedPassword = await hash(password) + hashedPassword = hashPassword ? await hash(password) : password } else if (dbUser) { hashedPassword = dbUser.password } else { @@ -110,6 +110,15 @@ exports.save = async ctx => { exports.adminUser = async ctx => { const { email, password, tenantId } = ctx.request.body + + // account portal sends a pre-hashed password - honour param to prevent double hashing + let hashPassword = ctx.request.query.hashPassword + if (hashPassword && hashPassword == "false") { + hashPassword = false + } else { + hashPassword = true + } + if (await doesTenantExist(tenantId)) { ctx.throw(403, "Organisation already exists.") } @@ -141,7 +150,7 @@ exports.adminUser = async ctx => { tenantId, } try { - ctx.body = await saveUser(user, tenantId) + ctx.body = await saveUser(user, tenantId, hashPassword) } catch (err) { ctx.throw(err.status || 400, err) }