diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index e677fe302c..6ea2834373 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1904,6 +1904,59 @@ describe.each([ }) }) }) + + describe("calculation views", () => { + it("should not remove calculation columns when modifying table schema", async () => { + let table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + age: { + name: "age", + type: FieldType.NUMBER, + }, + }, + }) + ) + + let view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "age", + }, + }, + }) + + 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 }, + }, + }, + }) + + view = await config.api.viewV2.get(view.id) + expect(Object.keys(view.schema!).sort()).toEqual([ + "age", + "id", + "name", + "sum", + ]) + }) + }) }) describe("row operations", () => { diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index b589d44b31..22b8d32978 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -20,29 +20,28 @@ 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)) +const validateViewSchemas: CustomValidator
= (table, joiHelpers) => { + if (!table.views || Object.keys(table.views).length === 0) { + return table + } + const required = Object.keys(table.schema).filter(key => + isRequired(table.schema[key].constraints) + ) + if (required.length === 0) { + return table + } + for (const view of Object.values(table.views)) { + if (!sdk.views.isV2(view) || helpers.views.isCalculationView(view)) { + continue + } + const editable = Object.entries(view.schema || {}) + .filter(([_, f]) => f.visible && !f.readonly) .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: `To make field "${missingField}" required, this field must be present and writable in views: ${view.name}.`, - }) - } - } + const missingField = required.find(f => !editable.includes(f)) + if (missingField) { + return joiHelpers.message({ + custom: `To make field "${missingField}" required, this field must be present and writable in views: ${view.name}.`, + }) } } return table diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 629454fecc..3549882199 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -96,6 +96,13 @@ async function guardCalculationViewSchema( continue } + if (!schema.field) { + throw new HTTPError( + `Calculation field "${name}" is missing a "field" property`, + 400 + ) + } + const targetSchema = table.schema[schema.field] if (!targetSchema) { throw new HTTPError( @@ -371,7 +378,8 @@ export function syncSchema( if (view.schema) { for (const fieldName of Object.keys(view.schema)) { - if (!schema[fieldName]) { + const viewSchema = view.schema[fieldName] + if (!helpers.views.isCalculationField(viewSchema) && !schema[fieldName]) { delete view.schema[fieldName] } }