diff --git a/packages/backend-core/src/middleware/joi-validator.ts b/packages/backend-core/src/middleware/joi-validator.ts
index ac8064a512..5047cdbbc1 100644
--- a/packages/backend-core/src/middleware/joi-validator.ts
+++ b/packages/backend-core/src/middleware/joi-validator.ts
@@ -3,7 +3,8 @@ import { Ctx } from "@budibase/types"
function validate(
schema: Joi.ObjectSchema | Joi.ArraySchema,
- property: string
+ property: string,
+ opts: { errorPrefix: string } = { errorPrefix: `Invalid ${property}` }
) {
// Return a Koa middleware function
return (ctx: Ctx, next: any) => {
@@ -29,16 +30,26 @@ function validate(
const { error } = schema.validate(params)
if (error) {
- ctx.throw(400, `Invalid ${property} - ${error.message}`)
+ let message = error.message
+ if (opts.errorPrefix) {
+ message = `Invalid ${property} - ${message}`
+ }
+ ctx.throw(400, message)
}
return next()
}
}
-export function body(schema: Joi.ObjectSchema | Joi.ArraySchema) {
- return validate(schema, "body")
+export function body(
+ schema: Joi.ObjectSchema | Joi.ArraySchema,
+ opts?: { errorPrefix: string }
+) {
+ return validate(schema, "body", opts)
}
-export function params(schema: Joi.ObjectSchema | Joi.ArraySchema) {
- return validate(schema, "params")
+export function params(
+ schema: Joi.ObjectSchema | Joi.ArraySchema,
+ opts?: { errorPrefix: string }
+) {
+ return validate(schema, "params", opts)
}
diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts
index 57a26dd6c7..b0ffd1e85c 100644
--- a/packages/server/src/api/routes/tests/viewV2.spec.ts
+++ b/packages/server/src/api/routes/tests/viewV2.spec.ts
@@ -368,7 +368,8 @@ describe.each([
await config.api.viewV2.create(newView, {
status: 400,
body: {
- message: 'You can\'t make read only the required field "name"',
+ message:
+ 'You can\'t make field "name" readonly because it is a required field.',
status: 400,
},
})
@@ -1479,7 +1480,7 @@ describe.each([
body: {
status: 400,
message:
- 'Invalid body - Required field "name" is missing in view "view a"',
+ 'To make field "name" required, this field must be present and writable in views: view a.',
},
}
)
@@ -1510,7 +1511,7 @@ describe.each([
body: {
status: 400,
message:
- 'Invalid body - Required field "name" is missing in view "view a"',
+ 'To make field "name" required, this field must be present and writable in views: view a.',
},
}
)
diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts
index 0a4eff4247..e2cc463f38 100644
--- a/packages/server/src/api/routes/utils/validators.ts
+++ b/packages/server/src/api/routes/utils/validators.ts
@@ -30,7 +30,7 @@ const validateViewSchemas: CustomValidator
= (table, helpers) => {
)
if (missingField) {
return helpers.message({
- custom: `Required field "${missingField}" is missing in view "${view.name}"`,
+ custom: `To make field "${missingField}" required, this field must be present and writable in views: ${view.name}.`,
})
}
}
@@ -40,42 +40,49 @@ const validateViewSchemas: CustomValidator = (table, helpers) => {
}
export function tableValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- _id: OPTIONAL_STRING,
- _rev: OPTIONAL_STRING,
- type: OPTIONAL_STRING.valid("table", "internal", "external"),
- primaryDisplay: OPTIONAL_STRING,
- schema: Joi.object().required(),
- name: Joi.string().required(),
- views: Joi.object(),
- rows: Joi.array(),
- }).custom(validateViewSchemas).unknown(true))
+ return auth.joiValidator.body(
+ Joi.object({
+ _id: OPTIONAL_STRING,
+ _rev: OPTIONAL_STRING,
+ type: OPTIONAL_STRING.valid("table", "internal", "external"),
+ primaryDisplay: OPTIONAL_STRING,
+ schema: Joi.object().required(),
+ name: Joi.string().required(),
+ views: Joi.object(),
+ rows: Joi.array(),
+ })
+ .custom(validateViewSchemas)
+ .unknown(true),
+ { errorPrefix: "" }
+ )
}
export function nameValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- name: OPTIONAL_STRING,
- }))
+ return auth.joiValidator.body(
+ Joi.object({
+ name: OPTIONAL_STRING,
+ })
+ )
}
export function datasourceValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- _id: Joi.string(),
- _rev: Joi.string(),
- type: OPTIONAL_STRING.allow("datasource_plus"),
- relationships: Joi.array().items(Joi.object({
- from: Joi.string().required(),
- to: Joi.string().required(),
- cardinality: Joi.valid("1:N", "1:1", "N:N").required()
- })),
- }).unknown(true))
+ return auth.joiValidator.body(
+ Joi.object({
+ _id: Joi.string(),
+ _rev: Joi.string(),
+ type: OPTIONAL_STRING.allow("datasource_plus"),
+ relationships: Joi.array().items(
+ Joi.object({
+ from: Joi.string().required(),
+ to: Joi.string().required(),
+ cardinality: Joi.valid("1:N", "1:1", "N:N").required(),
+ })
+ ),
+ }).unknown(true)
+ )
}
function filterObject() {
- // prettier-ignore
return Joi.object({
string: Joi.object().optional(),
fuzzy: Joi.object().optional(),
@@ -92,17 +99,20 @@ function filterObject() {
}
export function internalSearchValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- tableId: OPTIONAL_STRING,
- query: filterObject(),
- limit: OPTIONAL_NUMBER,
- sort: OPTIONAL_STRING,
- sortOrder: OPTIONAL_STRING,
- sortType: OPTIONAL_STRING,
- paginate: Joi.boolean(),
- bookmark: Joi.alternatives().try(OPTIONAL_STRING, OPTIONAL_NUMBER).optional(),
- }))
+ return auth.joiValidator.body(
+ Joi.object({
+ tableId: OPTIONAL_STRING,
+ query: filterObject(),
+ limit: OPTIONAL_NUMBER,
+ sort: OPTIONAL_STRING,
+ sortOrder: OPTIONAL_STRING,
+ sortType: OPTIONAL_STRING,
+ paginate: Joi.boolean(),
+ bookmark: Joi.alternatives()
+ .try(OPTIONAL_STRING, OPTIONAL_NUMBER)
+ .optional(),
+ })
+ )
}
export function externalSearchValidator() {
@@ -124,92 +134,110 @@ export function externalSearchValidator() {
}
export function datasourceQueryValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- endpoint: Joi.object({
- datasourceId: Joi.string().required(),
- operation: Joi.string().required().valid(...Object.values(DataSourceOperation)),
- entityId: Joi.string().required(),
- }).required(),
- resource: Joi.object({
- fields: Joi.array().items(Joi.string()).optional(),
- }).optional(),
- body: Joi.object().optional(),
- sort: Joi.object().optional(),
- filters: filterObject().optional(),
- paginate: Joi.object({
- page: Joi.string().alphanum().optional(),
- limit: Joi.number().optional(),
- }).optional(),
- }))
+ return auth.joiValidator.body(
+ Joi.object({
+ endpoint: Joi.object({
+ datasourceId: Joi.string().required(),
+ operation: Joi.string()
+ .required()
+ .valid(...Object.values(DataSourceOperation)),
+ entityId: Joi.string().required(),
+ }).required(),
+ resource: Joi.object({
+ fields: Joi.array().items(Joi.string()).optional(),
+ }).optional(),
+ body: Joi.object().optional(),
+ sort: Joi.object().optional(),
+ filters: filterObject().optional(),
+ paginate: Joi.object({
+ page: Joi.string().alphanum().optional(),
+ limit: Joi.number().optional(),
+ }).optional(),
+ })
+ )
}
export function webhookValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- live: Joi.bool(),
- _id: OPTIONAL_STRING,
- _rev: OPTIONAL_STRING,
- name: Joi.string().required(),
- bodySchema: Joi.object().optional(),
- action: Joi.object({
- type: Joi.string().required().valid(WebhookActionType.AUTOMATION),
- target: Joi.string().required(),
- }).required(),
- }).unknown(true))
+ return auth.joiValidator.body(
+ Joi.object({
+ live: Joi.bool(),
+ _id: OPTIONAL_STRING,
+ _rev: OPTIONAL_STRING,
+ name: Joi.string().required(),
+ bodySchema: Joi.object().optional(),
+ action: Joi.object({
+ type: Joi.string().required().valid(WebhookActionType.AUTOMATION),
+ target: Joi.string().required(),
+ }).required(),
+ }).unknown(true)
+ )
}
export function roleValidator() {
const permLevelArray = Object.values(permissions.PermissionLevel)
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- _id: OPTIONAL_STRING,
- _rev: OPTIONAL_STRING,
- name: Joi.string().regex(/^[a-zA-Z0-9_]*$/).required(),
- // this is the base permission ID (for now a built in)
- permissionId: Joi.string().valid(...Object.values(permissions.BuiltinPermissionID)).required(),
- permissions: Joi.object()
- .pattern(/.*/, [Joi.string().valid(...permLevelArray)])
- .optional(),
- inherits: OPTIONAL_STRING,
- }).unknown(true))
+
+ return auth.joiValidator.body(
+ Joi.object({
+ _id: OPTIONAL_STRING,
+ _rev: OPTIONAL_STRING,
+ name: Joi.string()
+ .regex(/^[a-zA-Z0-9_]*$/)
+ .required(),
+ // this is the base permission ID (for now a built in)
+ permissionId: Joi.string()
+ .valid(...Object.values(permissions.BuiltinPermissionID))
+ .required(),
+ permissions: Joi.object()
+ .pattern(/.*/, [Joi.string().valid(...permLevelArray)])
+ .optional(),
+ inherits: OPTIONAL_STRING,
+ }).unknown(true)
+ )
}
export function permissionValidator() {
const permLevelArray = Object.values(permissions.PermissionLevel)
- // prettier-ignore
- return auth.joiValidator.params(Joi.object({
- level: Joi.string().valid(...permLevelArray).required(),
- resourceId: Joi.string(),
- roleId: Joi.string(),
- }).unknown(true))
+
+ return auth.joiValidator.params(
+ Joi.object({
+ level: Joi.string()
+ .valid(...permLevelArray)
+ .required(),
+ resourceId: Joi.string(),
+ roleId: Joi.string(),
+ }).unknown(true)
+ )
}
export function screenValidator() {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- name: Joi.string().required(),
- showNavigation: OPTIONAL_BOOLEAN,
- width: OPTIONAL_STRING,
- routing: Joi.object({
- route: Joi.string().required(),
- roleId: Joi.string().required().allow(""),
- homeScreen: OPTIONAL_BOOLEAN,
- }).required().unknown(true),
- props: Joi.object({
- _id: Joi.string().required(),
- _component: Joi.string().required(),
- _children: Joi.array().required(),
- _styles: Joi.object().required(),
- type: OPTIONAL_STRING,
- table: OPTIONAL_STRING,
- layoutId: OPTIONAL_STRING,
- }).required().unknown(true),
- }).unknown(true))
+ return auth.joiValidator.body(
+ Joi.object({
+ name: Joi.string().required(),
+ showNavigation: OPTIONAL_BOOLEAN,
+ width: OPTIONAL_STRING,
+ routing: Joi.object({
+ route: Joi.string().required(),
+ roleId: Joi.string().required().allow(""),
+ homeScreen: OPTIONAL_BOOLEAN,
+ })
+ .required()
+ .unknown(true),
+ props: Joi.object({
+ _id: Joi.string().required(),
+ _component: Joi.string().required(),
+ _children: Joi.array().required(),
+ _styles: Joi.object().required(),
+ type: OPTIONAL_STRING,
+ table: OPTIONAL_STRING,
+ layoutId: OPTIONAL_STRING,
+ })
+ .required()
+ .unknown(true),
+ }).unknown(true)
+ )
}
function generateStepSchema(allowStepTypes: string[]) {
- // prettier-ignore
return Joi.object({
stepId: Joi.string().required(),
id: Joi.string().required(),
@@ -219,33 +247,39 @@ function generateStepSchema(allowStepTypes: string[]) {
icon: Joi.string().required(),
params: Joi.object(),
args: Joi.object(),
- type: Joi.string().required().valid(...allowStepTypes),
+ type: Joi.string()
+ .required()
+ .valid(...allowStepTypes),
}).unknown(true)
}
export function automationValidator(existing = false) {
- // prettier-ignore
- return auth.joiValidator.body(Joi.object({
- _id: existing ? Joi.string().required() : OPTIONAL_STRING,
- _rev: existing ? Joi.string().required() : OPTIONAL_STRING,
- name: Joi.string().required(),
- type: Joi.string().valid("automation").required(),
- definition: Joi.object({
- steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
- trigger: generateStepSchema(["TRIGGER"]).allow(null),
- }).required().unknown(true),
- }).unknown(true))
+ return auth.joiValidator.body(
+ Joi.object({
+ _id: existing ? Joi.string().required() : OPTIONAL_STRING,
+ _rev: existing ? Joi.string().required() : OPTIONAL_STRING,
+ name: Joi.string().required(),
+ type: Joi.string().valid("automation").required(),
+ definition: Joi.object({
+ steps: Joi.array()
+ .required()
+ .items(generateStepSchema(["ACTION", "LOGIC"])),
+ trigger: generateStepSchema(["TRIGGER"]).allow(null),
+ })
+ .required()
+ .unknown(true),
+ }).unknown(true)
+ )
}
export function applicationValidator(opts = { isCreate: true }) {
- // prettier-ignore
const base: any = {
_id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING,
url: OPTIONAL_STRING,
template: Joi.object({
templateString: OPTIONAL_STRING,
- })
+ }),
}
const appNameValidator = Joi.string()
diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts
index 98871539c2..52ab323faa 100644
--- a/packages/server/src/sdk/app/views/index.ts
+++ b/packages/server/src/sdk/app/views/index.ts
@@ -84,7 +84,7 @@ async function guardViewSchema(
if (viewSchemaField.readonly) {
throw new HTTPError(
- `You can't make read only the required field "${field.name}"`,
+ `You can't make field "${field.name}" readonly because it is a required field.`,
400
)
}