diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index 7d14fd0e87..30f86a79d1 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -101,7 +101,13 @@ } catch (error) { console.error(error) analytics.captureException(error) - notifications.error("Error publishing app") + const baseMsg = "Error publishing app" + const message = error.message + if (message) { + notifications.error(`${baseMsg} - ${message}`) + } else { + notifications.error(baseMsg) + } } publishing = false } diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 0c28787f67..b1f463e363 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -16,6 +16,7 @@ import { } from "@budibase/types" import sdk from "../sdk" import { automationsEnabled } from "../features" +import { helpers } from "@budibase/shared-core" import tracer from "dd-trace" const REBOOT_CRON = "@reboot" @@ -198,6 +199,13 @@ export async function enableCronTrigger(appId: any, automation: Automation) { !isRebootTrigger(automation) && trigger?.inputs.cron ) { + const cronExp = trigger.inputs.cron + const validation = helpers.cron.validate(cronExp) + if (!validation.valid) { + throw new Error( + `Invalid automation CRON "${cronExp}" - ${validation.err.join(", ")}` + ) + } // make a job id rather than letting Bull decide, makes it easier to handle on way out const jobId = `${appId}_cron_${newid()}` const job: any = await automationQueue.add( @@ -205,7 +213,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) { automation, event: { appId, timestamp: Date.now() }, }, - { repeat: { cron: trigger.inputs.cron }, jobId } + { repeat: { cron: cronExp }, jobId } ) // Assign cron job ID from bull so we can remove it later if the cron trigger is removed trigger.cronJobId = job.id diff --git a/packages/shared-core/package.json b/packages/shared-core/package.json index edbbd1dc56..12a94f5a2f 100644 --- a/packages/shared-core/package.json +++ b/packages/shared-core/package.json @@ -14,7 +14,8 @@ "check:types": "tsc -p tsconfig.json --noEmit --paths null" }, "dependencies": { - "@budibase/types": "0.0.0" + "@budibase/types": "0.0.0", + "cron-validate": "^1.4.5" }, "devDependencies": { "rimraf": "3.0.2", diff --git a/packages/shared-core/src/helpers/cron.ts b/packages/shared-core/src/helpers/cron.ts new file mode 100644 index 0000000000..e83738f7cd --- /dev/null +++ b/packages/shared-core/src/helpers/cron.ts @@ -0,0 +1,47 @@ +import cronValidate from "cron-validate" + +const INPUT_CRON_START = "(Input cron: " +const ERROR_SWAPS = { + "smaller than lower limit": "less than", + "bigger than upper limit": "greater than", + daysOfMonth: "'days of the month'", + daysOfWeek: "'days of the week'", + years: "'years'", + months: "'months'", + hours: "'hours'", + minutes: "'minutes'", + seconds: "'seconds'", +} + +function improveErrors(errors: string[]): string[] { + const finalErrors: string[] = [] + + for (let error of errors) { + if (error.includes(INPUT_CRON_START)) { + error = error.split(INPUT_CRON_START)[0].trim() + } + for (let [key, value] of Object.entries(ERROR_SWAPS)) { + if (error.includes(key)) { + error = error.replace(new RegExp(key, "g"), value) + } + } + finalErrors.push(error) + } + return finalErrors +} + +export function validate( + cronExpression: string +): { valid: false; err: string[] } | { valid: true } { + const result = cronValidate(cronExpression, { + preset: "npm-node-cron", + override: { + useSeconds: false, + }, + }) + if (!result.isValid()) { + return { valid: false, err: improveErrors(result.getError()) } + } else { + return { valid: true } + } +} diff --git a/packages/shared-core/src/helpers/index.ts b/packages/shared-core/src/helpers/index.ts index fd185aa1e9..e76022b14b 100644 --- a/packages/shared-core/src/helpers/index.ts +++ b/packages/shared-core/src/helpers/index.ts @@ -1,2 +1,3 @@ export * from "./helpers" export * from "./integrations" +export * as cron from "./cron" diff --git a/packages/shared-core/src/tests/cron.test.ts b/packages/shared-core/src/tests/cron.test.ts new file mode 100644 index 0000000000..d56165b2b8 --- /dev/null +++ b/packages/shared-core/src/tests/cron.test.ts @@ -0,0 +1,22 @@ +import { expect, describe, it } from "vitest" +import { cron } from "../helpers" + +describe("check valid and invalid crons", () => { + it("invalid - 0 0 0 11 *", () => { + expect(cron.validate("0 0 0 11 *")).toStrictEqual({ + valid: false, + err: [expect.stringContaining("less than '1'")], + }) + }) + + it("invalid - 5 4 32 1 1", () => { + expect(cron.validate("5 4 32 1 1")).toStrictEqual({ + valid: false, + err: [expect.stringContaining("greater than '31'")], + }) + }) + + it("valid - * * * * *", () => { + expect(cron.validate("* * * * *")).toStrictEqual({ valid: true }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 9e12ecad89..defee1e6e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1980,6 +1980,13 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== +"@babel/runtime@^7.10.5": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" + integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.23.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" @@ -5431,6 +5438,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== +"@types/lodash@^4.14.165": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + "@types/long@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -8541,6 +8553,13 @@ cron-parser@^4.2.1: dependencies: luxon "^3.2.1" +cron-validate@^1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/cron-validate/-/cron-validate-1.4.5.tgz#eceb221f7558e6302e5f84c7b3a454fdf4d064c3" + integrity sha512-nKlOJEnYKudMn/aNyNH8xxWczlfpaazfWV32Pcx/2St51r2bxWbGhZD7uwzMcRhunA/ZNL+Htm/i0792Z59UMQ== + dependencies: + yup "0.32.9" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -14440,7 +14459,7 @@ lock@^1.1.0: resolved "https://registry.yarnpkg.com/lock/-/lock-1.1.0.tgz#53157499d1653b136ca66451071fca615703fa55" integrity sha512-NZQIJJL5Rb9lMJ0Yl1JoVr9GSdo4HTPsUEWsSFzB8dE8DSoiLCVavWZPi7Rnlv/o73u6I24S/XYc/NmG4l8EKA== -lodash-es@^4.17.21: +lodash-es@^4.17.15, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -14590,7 +14609,7 @@ lodash.xor@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.xor/-/lodash.xor-4.5.0.tgz#4d48ed7e98095b0632582ba714d3ff8ae8fb1db6" integrity sha512-sVN2zimthq7aZ5sPGXnSz32rZPuqcparVW50chJQe+mzTYV+IsxSsl/2gnkWWE2Of7K3myBQBqtLKOUEHJKRsQ== -lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -22020,6 +22039,19 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +yup@0.32.9: + version "0.32.9" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872" + integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg== + dependencies: + "@babel/runtime" "^7.10.5" + "@types/lodash" "^4.14.165" + lodash "^4.17.20" + lodash-es "^4.17.15" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2" + yup@^0.32.11: version "0.32.11" resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"