diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index 0087d377ad..76807b796a 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -34,7 +34,7 @@ async function parseSchema(view: CreateViewRequest) { return p }, {} as Record>) for (let [key, column] of Object.entries(finalViewSchema)) { - if (!column.visible) { + if (!column.visible && !column.readonly) { delete finalViewSchema[key] } } diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 8469bc52f7..aad8736a62 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -23,6 +23,14 @@ 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", () => { + return { + __esModule: true, + ...jest.requireActual("../../../utilities/schema"), + } +}) describe.each([ ["internal", undefined], @@ -260,6 +268,45 @@ describe.each([ }, }) }) + + it("required fields cannot be marked as readonly", async () => { + const isRequiredSpy = jest.spyOn(schemaUtils, "isRequired") + + isRequiredSpy.mockReturnValue(true) + + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + name: { + readonly: true, + }, + }, + } + + await config.api.viewV2.create(newView, { + status: 400, + body: { + message: 'Field "name" cannot be readonly as it is a required field', + status: 400, + }, + }) + }) }) describe("update", () => { diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index e62931b934..68a689fb78 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -15,6 +15,7 @@ import { isExternalTableID } from "../../../integrations/utils" import * as internal from "./internal" import * as external from "./external" import sdk from "../../../sdk" +import { isRequired } from "../../../utilities/schema" function pickApi(tableId: any) { if (isExternalTableID(tableId)) { @@ -35,20 +36,31 @@ export async function getEnriched(viewId: string): Promise { async function guardViewSchema( tableId: string, - schema?: Record + viewSchema?: Record ) { - if (!schema || !Object.keys(schema).length) { + if (!viewSchema || !Object.keys(viewSchema).length) { return } const table = await sdk.tables.getTable(tableId) - if (schema) { - for (const field of Object.keys(schema)) { - if (!table.schema[field]) { + if (viewSchema) { + for (const field of Object.keys(viewSchema)) { + const tableSchemaField = table.schema[field] + if (!tableSchemaField) { throw new HTTPError( `Field "${field}" is not valid for the requested table`, 400 ) } + + if ( + viewSchema[field].readonly && + isRequired(tableSchemaField.constraints) + ) { + throw new HTTPError( + `Field "${field}" cannot be readonly as it is a required field`, + 400 + ) + } } } } diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts index f73701fdfd..421d3ef8f2 100644 --- a/packages/server/src/utilities/schema.ts +++ b/packages/server/src/utilities/schema.ts @@ -4,6 +4,7 @@ import { TableSchema, FieldSchema, Row, + FieldConstraints, } from "@budibase/types" import { ValidColumnNameRegex, utils } from "@budibase/shared-core" import { db } from "@budibase/backend-core" @@ -40,6 +41,15 @@ export function isRows(rows: any): rows is Rows { return Array.isArray(rows) && rows.every(row => typeof row === "object") } +export function isRequired(constraints: FieldConstraints | undefined) { + const isRequired = + !!constraints && + ((typeof constraints.presence !== "boolean" && + !constraints.presence?.allowEmpty) || + constraints.presence === true) + return isRequired +} + export function validate(rows: Rows, schema: TableSchema): ValidationResults { const results: ValidationResults = { schemaValidation: {}, @@ -62,12 +72,6 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults { return } - const isRequired = - !!constraints && - ((typeof constraints.presence !== "boolean" && - !constraints.presence?.allowEmpty) || - constraints.presence === true) - // If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array if (typeof columnType !== "string") { results.invalidColumns.push(columnName) @@ -101,7 +105,12 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults { } else if ( (columnType === FieldType.BB_REFERENCE || columnType === FieldType.BB_REFERENCE_SINGLE) && - !isValidBBReference(columnData, columnType, columnSubtype, isRequired) + !isValidBBReference( + columnData, + columnType, + columnSubtype, + isRequired(constraints) + ) ) { results.schemaValidation[columnName] = false } else {