Validate schema
This commit is contained in:
parent
5912c2b129
commit
efc9d3399e
|
@ -23,9 +23,6 @@ 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")
|
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["internal", undefined],
|
["internal", undefined],
|
||||||
|
@ -318,15 +315,13 @@ describe.each([
|
||||||
})
|
})
|
||||||
|
|
||||||
it("required fields cannot be marked as readonly", async () => {
|
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(
|
const table = await config.api.table.save(
|
||||||
saveTableRequest({
|
saveTableRequest({
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
name: "name",
|
name: "name",
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
|
constraints: { presence: true },
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
name: "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"',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,44 @@
|
||||||
import { auth, permissions } from "@budibase/backend-core"
|
import { auth, permissions } from "@budibase/backend-core"
|
||||||
import { DataSourceOperation } from "../../../constants"
|
import { DataSourceOperation } from "../../../constants"
|
||||||
import { WebhookActionType } from "@budibase/types"
|
import { Table, WebhookActionType } from "@budibase/types"
|
||||||
import Joi from "joi"
|
import Joi, { CustomValidator } from "joi"
|
||||||
import { ValidSnippetNameRegex } from "@budibase/shared-core"
|
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_STRING = Joi.string().optional().allow(null).allow("")
|
||||||
const OPTIONAL_NUMBER = Joi.number().optional().allow(null)
|
const OPTIONAL_NUMBER = Joi.number().optional().allow(null)
|
||||||
const OPTIONAL_BOOLEAN = Joi.boolean().optional().allow(null)
|
const OPTIONAL_BOOLEAN = Joi.boolean().optional().allow(null)
|
||||||
const APP_NAME_REGEX = /^[\w\s]+$/
|
const APP_NAME_REGEX = /^[\w\s]+$/
|
||||||
|
|
||||||
|
const validateViewSchemas: CustomValidator<Table> = (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() {
|
export function tableValidator() {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return auth.joiValidator.body(Joi.object({
|
return auth.joiValidator.body(Joi.object({
|
||||||
|
@ -20,7 +50,7 @@ export function tableValidator() {
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
views: Joi.object(),
|
views: Joi.object(),
|
||||||
rows: Joi.array(),
|
rows: Joi.array(),
|
||||||
}).unknown(true))
|
}).custom(validateViewSchemas).unknown(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nameValidator() {
|
export function nameValidator() {
|
||||||
|
|
Loading…
Reference in New Issue