1
0
Fork 0
mirror of synced 2024-09-10 06:26:02 +12:00

Guard readonly fields

This commit is contained in:
Adria Navarro 2024-05-27 13:39:43 +02:00
parent 6acb3f6669
commit 65d2aa50c6
4 changed files with 81 additions and 13 deletions

View file

@ -34,7 +34,7 @@ async function parseSchema(view: CreateViewRequest) {
return p return p
}, {} as Record<string, RequiredKeys<ViewUIFieldMetadata>>) }, {} as Record<string, RequiredKeys<ViewUIFieldMetadata>>)
for (let [key, column] of Object.entries(finalViewSchema)) { for (let [key, column] of Object.entries(finalViewSchema)) {
if (!column.visible) { if (!column.visible && !column.readonly) {
delete finalViewSchema[key] delete finalViewSchema[key]
} }
} }

View file

@ -23,6 +23,14 @@ import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
import merge from "lodash/merge" import merge from "lodash/merge"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { roles } from "@budibase/backend-core" 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([ describe.each([
["internal", undefined], ["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", () => { describe("update", () => {

View file

@ -15,6 +15,7 @@ import { isExternalTableID } from "../../../integrations/utils"
import * as internal from "./internal" import * as internal from "./internal"
import * as external from "./external" import * as external from "./external"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { isRequired } from "../../../utilities/schema"
function pickApi(tableId: any) { function pickApi(tableId: any) {
if (isExternalTableID(tableId)) { if (isExternalTableID(tableId)) {
@ -35,20 +36,31 @@ export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
async function guardViewSchema( async function guardViewSchema(
tableId: string, tableId: string,
schema?: Record<string, ViewUIFieldMetadata> viewSchema?: Record<string, ViewUIFieldMetadata>
) { ) {
if (!schema || !Object.keys(schema).length) { if (!viewSchema || !Object.keys(viewSchema).length) {
return return
} }
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
if (schema) { if (viewSchema) {
for (const field of Object.keys(schema)) { for (const field of Object.keys(viewSchema)) {
if (!table.schema[field]) { const tableSchemaField = table.schema[field]
if (!tableSchemaField) {
throw new HTTPError( throw new HTTPError(
`Field "${field}" is not valid for the requested table`, `Field "${field}" is not valid for the requested table`,
400 400
) )
} }
if (
viewSchema[field].readonly &&
isRequired(tableSchemaField.constraints)
) {
throw new HTTPError(
`Field "${field}" cannot be readonly as it is a required field`,
400
)
}
} }
} }
} }

View file

@ -4,6 +4,7 @@ import {
TableSchema, TableSchema,
FieldSchema, FieldSchema,
Row, Row,
FieldConstraints,
} from "@budibase/types" } from "@budibase/types"
import { ValidColumnNameRegex, utils } from "@budibase/shared-core" import { ValidColumnNameRegex, utils } from "@budibase/shared-core"
import { db } from "@budibase/backend-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") 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 { export function validate(rows: Rows, schema: TableSchema): ValidationResults {
const results: ValidationResults = { const results: ValidationResults = {
schemaValidation: {}, schemaValidation: {},
@ -62,12 +72,6 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
return 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 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") { if (typeof columnType !== "string") {
results.invalidColumns.push(columnName) results.invalidColumns.push(columnName)
@ -101,7 +105,12 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
} else if ( } else if (
(columnType === FieldType.BB_REFERENCE || (columnType === FieldType.BB_REFERENCE ||
columnType === FieldType.BB_REFERENCE_SINGLE) && columnType === FieldType.BB_REFERENCE_SINGLE) &&
!isValidBBReference(columnData, columnType, columnSubtype, isRequired) !isValidBBReference(
columnData,
columnType,
columnSubtype,
isRequired(constraints)
)
) { ) {
results.schemaValidation[columnName] = false results.schemaValidation[columnName] = false
} else { } else {