From 5912c2b129f9888268a95afa23514668dcb97d43 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 May 2024 13:30:05 +0200 Subject: [PATCH 01/10] Copy change --- packages/server/src/api/routes/tests/viewV2.spec.ts | 3 +-- packages/server/src/sdk/app/views/index.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 962d6e82a3..7e02966b44 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -349,8 +349,7 @@ describe.each([ await config.api.viewV2.create(newView, { status: 400, body: { - message: - 'Field "name" cannot be readonly as it is a required field', + message: 'You can\'t make the required field "name" read only', status: 400, }, }) diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 18ab94be21..150fe2c678 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -60,7 +60,7 @@ async function guardViewSchema( if (isRequired(tableSchemaField.constraints)) { throw new HTTPError( - `Field "${field}" cannot be readonly as it is a required field`, + `You can't make the required field "${field}" read only`, 400 ) } From efc9d3399e11c20daf744e30194bcc7aac79e195 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 31 May 2024 17:08:50 +0200 Subject: [PATCH 02/10] Validate schema --- .../src/api/routes/tests/viewV2.spec.ts | 126 +++++++++++++++++- .../server/src/api/routes/utils/validators.ts | 36 ++++- 2 files changed, 153 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 7e02966b44..af5cbfc063 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -23,9 +23,6 @@ import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" import { roles } from "@budibase/backend-core" -import * as schemaUtils from "../../../utilities/schema" - -jest.mock("../../../utilities/schema") describe.each([ ["internal", undefined], @@ -318,15 +315,13 @@ describe.each([ }) it("required fields cannot be marked as readonly", async () => { - const isRequiredSpy = jest.spyOn(schemaUtils, "isRequired") - isRequiredSpy.mockReturnValueOnce(true) - const table = await config.api.table.save( saveTableRequest({ schema: { name: { name: "name", type: FieldType.STRING, + constraints: { presence: true }, }, description: { name: "description", @@ -1347,4 +1342,123 @@ describe.each([ }) }) }) + + describe("updating table schema", () => { + describe("existing columns changed to required", () => { + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + id: { + name: "id", + type: FieldType.AUTO, + autocolumn: true, + }, + name: { + name: "name", + type: FieldType.STRING, + }, + }, + }) + ) + }) + + it("allows updating when no views constrains the field", async () => { + await config.api.viewV2.create({ + name: "view a", + tableId: table._id!, + schema: { + id: { visible: true }, + name: { visible: true }, + }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: { allowEmpty: false } }, + }, + }, + }, + { status: 200 } + ) + }) + + it("rejects if field is readonly in any view", async () => { + mocks.licenses.useViewReadonlyColumns() + + await config.api.viewV2.create({ + name: "view a", + tableId: table._id!, + schema: { + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + }, + }, + { + status: 400, + body: { + status: 400, + message: + 'Invalid body - Required field "name" is missing in view "view a"', + }, + } + ) + }) + + it("rejects if field is hidden in any view", async () => { + await config.api.viewV2.create({ + name: "view a", + tableId: table._id!, + schema: { id: { visible: true } }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + }, + }, + { + status: 400, + body: { + status: 400, + message: + 'Invalid body - Required field "name" is missing in view "view a"', + }, + } + ) + }) + }) + }) }) diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index 424d0d6c79..0a4eff4247 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -1,14 +1,44 @@ import { auth, permissions } from "@budibase/backend-core" import { DataSourceOperation } from "../../../constants" -import { WebhookActionType } from "@budibase/types" -import Joi from "joi" +import { Table, WebhookActionType } from "@budibase/types" +import Joi, { CustomValidator } from "joi" import { ValidSnippetNameRegex } from "@budibase/shared-core" +import { isRequired } from "../../../utilities/schema" +import sdk from "../../../sdk" const OPTIONAL_STRING = Joi.string().optional().allow(null).allow("") const OPTIONAL_NUMBER = Joi.number().optional().allow(null) const OPTIONAL_BOOLEAN = Joi.boolean().optional().allow(null) const APP_NAME_REGEX = /^[\w\s]+$/ +const validateViewSchemas: CustomValidator = (table, helpers) => { + if (table.views && Object.entries(table.views).length) { + const requiredFields = Object.entries(table.schema) + .filter(([_, v]) => isRequired(v.constraints)) + .map(([key]) => key) + if (requiredFields.length) { + for (const view of Object.values(table.views)) { + if (!sdk.views.isV2(view)) { + continue + } + + const editableViewFields = Object.entries(view.schema || {}) + .filter(([_, f]) => f.visible && !f.readonly) + .map(([key]) => key) + const missingField = requiredFields.find( + f => !editableViewFields.includes(f) + ) + if (missingField) { + return helpers.message({ + custom: `Required field "${missingField}" is missing in view "${view.name}"`, + }) + } + } + } + } + return table +} + export function tableValidator() { // prettier-ignore return auth.joiValidator.body(Joi.object({ @@ -20,7 +50,7 @@ export function tableValidator() { name: Joi.string().required(), views: Joi.object(), rows: Joi.array(), - }).unknown(true)) + }).custom(validateViewSchemas).unknown(true)) } export function nameValidator() { From 326a90a41e550b9859933fc7531b547efd06c622 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 May 2024 14:37:19 +0200 Subject: [PATCH 03/10] Allow modifying views with readonly configs (other fields) --- packages/server/src/sdk/app/views/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 150fe2c678..292d643648 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -54,7 +54,10 @@ async function guardViewSchema( } if (viewSchema[field].readonly) { - if (!(await features.isViewReadonlyColumnsEnabled())) { + if ( + !(await features.isViewReadonlyColumnsEnabled()) && + !(tableSchemaField as ViewUIFieldMetadata).readonly + ) { throw new HTTPError(`Readonly fields are not enabled`, 400) } From dad689c78700c1344abffbf01276889d6a6deb46 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 30 May 2024 14:37:35 +0200 Subject: [PATCH 04/10] Reset schema mutations on erroring --- .../src/components/grid/controls/ColumnsSettingButton.svelte | 2 ++ .../frontend-core/src/components/grid/stores/datasource.js | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte index 3f0e2341be..aa27871f92 100644 --- a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte +++ b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte @@ -24,6 +24,8 @@ await datasource.actions.saveSchemaMutations() } catch (e) { notifications.error(e.message) + } finally { + datasource.actions.resetSchemaMutations() } dispatch(visible ? "show-column" : "hide-column") } diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 1fc973f171..09b8be4868 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -204,6 +204,10 @@ export const createActions = context => { ...$definition, schema: newSchema, }) + resetSchemaMutations() + } + + const resetSchemaMutations = () => { schemaMutations.set({}) } @@ -253,6 +257,7 @@ export const createActions = context => { addSchemaMutation, addSchemaMutations, saveSchemaMutations, + resetSchemaMutations, }, }, } From d73d7113aedd31a4674ca4560080f34b98d67f5f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 3 Jun 2024 10:07:42 +0200 Subject: [PATCH 05/10] Refresh on error --- .../components/grid/controls/ColumnsSettingButton.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte index aa27871f92..228cf69e34 100644 --- a/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte +++ b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte @@ -19,13 +19,17 @@ const visible = permission !== PERMISSION_OPTIONS.HIDDEN const readonly = permission === PERMISSION_OPTIONS.READONLY - datasource.actions.addSchemaMutation(column.name, { visible, readonly }) + await datasource.actions.addSchemaMutation(column.name, { + visible, + readonly, + }) try { await datasource.actions.saveSchemaMutations() } catch (e) { notifications.error(e.message) } finally { - datasource.actions.resetSchemaMutations() + await datasource.actions.resetSchemaMutations() + await datasource.actions.refreshDefinition() } dispatch(visible ? "show-column" : "hide-column") } From 91c20213dc20341d1cee5dacc27b7a957ffadc37 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Mon, 3 Jun 2024 10:29:26 +0200 Subject: [PATCH 06/10] Validate readonly --- .../src/api/routes/tests/viewV2.spec.ts | 3 ++- packages/server/src/sdk/app/views/index.ts | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index af5cbfc063..4e98ffe3cc 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -336,6 +336,7 @@ describe.each([ tableId: table._id!, schema: { name: { + visible: true, readonly: true, }, }, @@ -344,7 +345,7 @@ describe.each([ await config.api.viewV2.create(newView, { status: 400, body: { - message: 'You can\'t make the required field "name" read only', + message: 'You can\'t make read only the required field "name"', status: 400, }, }) diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 292d643648..ff51a73e99 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -39,9 +39,7 @@ async function guardViewSchema( tableId: string, viewSchema?: Record ) { - if (!viewSchema || !Object.keys(viewSchema).length) { - return - } + viewSchema ??= {} const table = await sdk.tables.getTable(tableId) for (const field of Object.keys(viewSchema)) { @@ -61,13 +59,6 @@ async function guardViewSchema( throw new HTTPError(`Readonly fields are not enabled`, 400) } - if (isRequired(tableSchemaField.constraints)) { - throw new HTTPError( - `You can't make the required field "${field}" read only`, - 400 - ) - } - if (!viewSchema[field].visible) { throw new HTTPError( `Field "${field}" must be visible if you want to make it readonly`, @@ -76,6 +67,20 @@ async function guardViewSchema( } } } + + for (const field of Object.values(table.schema)) { + if (!isRequired(field.constraints)) { + continue + } + + const viewSchemaField = viewSchema[field.name] + if (viewSchemaField?.readonly) { + throw new HTTPError( + `You can't make read only the required field "${field.name}"`, + 400 + ) + } + } } export async function create( From 9b82116c61f7ea9854bea5cefe7dfd1be9c85c19 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 4 Jun 2024 10:39:56 +0200 Subject: [PATCH 07/10] Copy changes --- packages/server/src/api/routes/tests/viewV2.spec.ts | 3 ++- packages/server/src/sdk/app/views/index.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 4e98ffe3cc..663cf5c864 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -345,7 +345,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, }, }) diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index ff51a73e99..6cdd38bf43 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -76,7 +76,7 @@ async function guardViewSchema( const viewSchemaField = viewSchema[field.name] 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 ) } From 2d953f19cc37a47756a0655cacf7273b19bf0333 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 4 Jun 2024 11:11:50 +0200 Subject: [PATCH 08/10] Clean validation message --- .../src/middleware/joi-validator.ts | 23 +- .../server/src/api/routes/utils/validators.ts | 276 ++++++++++-------- 2 files changed, 172 insertions(+), 127 deletions(-) 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/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index 0a4eff4247..9a726dc757 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -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() From 819cc6bebb8c2db492c2bbfe8641200bf8dc5b47 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 4 Jun 2024 11:18:33 +0200 Subject: [PATCH 09/10] Fix tests --- packages/server/src/api/routes/tests/viewV2.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 663cf5c864..3376cb86fa 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1424,8 +1424,7 @@ describe.each([ status: 400, body: { status: 400, - message: - 'Invalid body - Required field "name" is missing in view "view a"', + message: 'Required field "name" is missing in view "view a"', }, } ) @@ -1455,8 +1454,7 @@ describe.each([ status: 400, body: { status: 400, - message: - 'Invalid body - Required field "name" is missing in view "view a"', + message: 'Required field "name" is missing in view "view a"', }, } ) From aefedce568168d116e9046b12278cfca1a4f69fa Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 4 Jun 2024 11:35:09 +0200 Subject: [PATCH 10/10] Renames --- packages/server/src/api/routes/tests/viewV2.spec.ts | 6 ++++-- packages/server/src/api/routes/utils/validators.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 3376cb86fa..cf9db761ce 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1424,7 +1424,8 @@ describe.each([ status: 400, body: { status: 400, - message: 'Required field "name" is missing in view "view a"', + message: + 'To make field "name" required, this field must be present and writable in views: view a.', }, } ) @@ -1454,7 +1455,8 @@ describe.each([ status: 400, body: { status: 400, - message: 'Required field "name" is missing in view "view a"', + message: + '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 9a726dc757..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}.`, }) } }