Merge pull request #13005 from Budibase/fix/add-cron-validation
Frontend + Backend CRON validation
This commit is contained in:
commit
dcb0ec191c
|
@ -2,15 +2,34 @@
|
|||
import { Button, Select, Input, Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { flags } from "stores/backend"
|
||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
let error
|
||||
|
||||
$: {
|
||||
const exists = CRON_EXPRESSIONS.some(cron => cron.value === value)
|
||||
const customIndex = CRON_EXPRESSIONS.findIndex(
|
||||
cron => cron.label === "Custom"
|
||||
)
|
||||
|
||||
if (!exists && customIndex === -1) {
|
||||
CRON_EXPRESSIONS[0] = { label: "Custom", value: value }
|
||||
} else if (exists && customIndex !== -1) {
|
||||
CRON_EXPRESSIONS.splice(customIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = e => {
|
||||
if (e.detail === value) {
|
||||
if (value !== REBOOT_CRON) {
|
||||
error = helpers.cron.validate(e.detail).err
|
||||
}
|
||||
if (e.detail === value || error) {
|
||||
return
|
||||
}
|
||||
|
||||
value = e.detail
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
|
@ -41,7 +60,7 @@
|
|||
if (!$flags.cloud) {
|
||||
CRON_EXPRESSIONS.push({
|
||||
label: "Every Budibase Reboot",
|
||||
value: "@reboot",
|
||||
value: REBOOT_CRON,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -49,6 +68,7 @@
|
|||
|
||||
<div class="block-field">
|
||||
<Input
|
||||
{error}
|
||||
on:change={onChange}
|
||||
{value}
|
||||
on:blur={() => (touched = true)}
|
||||
|
@ -64,7 +84,7 @@
|
|||
{#if presets}
|
||||
<Select
|
||||
on:change={onChange}
|
||||
{value}
|
||||
value={value || "Custom"}
|
||||
secondary
|
||||
extraThin
|
||||
label="Presets"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ import {
|
|||
} from "@budibase/types"
|
||||
import sdk from "../sdk"
|
||||
import { automationsEnabled } from "../features"
|
||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||
import tracer from "dd-trace"
|
||||
|
||||
const REBOOT_CRON = "@reboot"
|
||||
const WH_STEP_ID = definitions.WEBHOOK.stepId
|
||||
const CRON_STEP_ID = definitions.CRON.stepId
|
||||
let Runner: Thread
|
||||
|
@ -198,6 +198,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 +212,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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -99,6 +99,8 @@ export const SocketSessionTTL = 60
|
|||
export const ValidQueryNameRegex = /^[^()]*$/
|
||||
export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g
|
||||
|
||||
export const REBOOT_CRON = "@reboot"
|
||||
|
||||
export const InvalidFileExtensions = [
|
||||
"7z",
|
||||
"action",
|
||||
|
|
|
@ -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 [oldErr, newErr] of Object.entries(ERROR_SWAPS)) {
|
||||
if (error.includes(oldErr)) {
|
||||
error = error.replace(new RegExp(oldErr, "g"), newErr)
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./helpers"
|
||||
export * from "./integrations"
|
||||
export * as cron from "./cron"
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
})
|
36
yarn.lock
36
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"
|
||||
|
|
Loading…
Reference in New Issue