diff --git a/apps/meta-data-utils/src/types.ts b/apps/meta-data-utils/src/types.ts index 108a5257ed..dadfee3e44 100644 --- a/apps/meta-data-utils/src/types.ts +++ b/apps/meta-data-utils/src/types.ts @@ -23,7 +23,7 @@ export interface IColumn { refLinkId?: string; refSchemaId?: string; refTableId?: string; - required?: boolean; + required?: string; semantics?: string[]; validation?: string; visible?: string; diff --git a/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts b/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts index d221486cca..101d60411e 100644 --- a/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts +++ b/apps/molgenis-components/src/components/forms/formUtils/formUtils.test.ts @@ -39,7 +39,7 @@ describe("getRowErrors", () => { id: "required", label: "required", columnType: "STRING", - required: true, + required: "true", }, ], } as ITableMetaData; @@ -55,7 +55,7 @@ describe("getRowErrors", () => { id: "required", label: "required", columnType: "DECIMAL", - required: true, + required: "true", }, ], } as ITableMetaData; @@ -63,6 +63,62 @@ describe("getRowErrors", () => { expect(result).to.deep.equal({ required: "required is required" }); }); + test("it should give an error if a field is conditionally required on another field", () => { + const rowData = { + status: null, + quantity: 6, + }; + const metaData = { + columns: [ + { + id: "status", + label: "status", + columnType: "STRING", + required: "if(quantity>5) 'if quantity > 5 required'", + }, + { + id: "quantity", + label: "quantity", + columnType: "DECIMAL", + required: "true", + }, + ], + } as ITableMetaData; + const result = getRowErrors(metaData, rowData); + expect(result).to.deep.equal({ + quantity: undefined, + status: "if quantity > 5 required", + }); + }); + + test("it should return undefined if a field is conditionally required on another field and provided", () => { + const rowData = { + status: "RECEIVED", + quantity: 6, + }; + const metaData = { + columns: [ + { + id: "status", + label: "status", + columnType: "STRING", + required: "if(quantity>5) 'if quantity > 5 required'", + }, + { + id: "quantity", + label: "quantity", + columnType: "DECIMAL", + required: "true", + }, + ], + } as ITableMetaData; + const result = getRowErrors(metaData, rowData); + expect(result).to.deep.equal({ + quantity: undefined, + status: undefined, + }); + }); + test("it should return undefined it has no value and isn't required", () => { const rowData = { empty: null }; const metaData = { diff --git a/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts b/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts index 12f195c120..2b22142603 100644 --- a/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts +++ b/apps/molgenis-components/src/components/forms/formUtils/formUtils.ts @@ -39,9 +39,21 @@ function getColumnError( if (column.columnType === AUTO_ID || column.columnType === HEADING) { return undefined; } - if (column.required && (missesValue || isInvalidNumber)) { - return column.label + " is required"; + if (column.required) { + if (column.required === "true") { + if (missesValue || isInvalidNumber) { + return column.label + " is required"; + } + } else { + let error = getRequiredExpressionError( + column.required, + rowData, + tableMetaData + ); + if (error && missesValue) return error; + } } + if (missesValue) { return undefined; } @@ -82,6 +94,24 @@ function isInValidNumericValue(columnType: string, value: number) { } } +function getRequiredExpressionError( + expression: string, + values: Record, + tableMetaData: ITableMetaData +): string | undefined { + try { + const result = executeExpression(expression, values, tableMetaData); + if (result === true) { + return `Field is required when: ${expression}`; + } else if (result === false || result === undefined) { + return undefined; + } + return result; + } catch (error) { + return `Invalid expression '${expression}', reason: ${error}`; + } +} + function getColumnValidationError( validation: string, values: Record, diff --git a/apps/schema/src/components/ColumnEditModal.vue b/apps/schema/src/components/ColumnEditModal.vue index 96ecb7780e..4296157142 100644 --- a/apps/schema/src/components/ColumnEditModal.vue +++ b/apps/schema/src/components/ColumnEditModal.vue @@ -114,11 +114,18 @@
- +
@@ -269,6 +276,7 @@ import { InputString, InputText, InputTextLocalized, + InputRadio, LayoutForm, LayoutModal, MessageError, @@ -296,6 +304,7 @@ export default { InputString, InputBoolean, InputSelect, + InputRadio, IconAction, InputTextLocalized, LayoutModal, @@ -349,6 +358,7 @@ export default { modalVisible: false, // working value of the column (copy of the value) column: null, + requiredSelect: null, //the type options columnTypes, //in case a refSchema has to be used for the table lookup @@ -472,6 +482,9 @@ export default { this.reset(); this.modalVisible = false; }, + setRequired() { + this.column.required = this.requiredSelect; + }, refLinkCandidates() { return this.table.columns .filter( @@ -507,6 +520,16 @@ export default { } this.loading = false; }, + setupRequiredSelect() { + console.log(this.column.required); + if (this.column.required === "true") { + this.requiredSelect = true; + } else if (this.column.required === "false") { + this.requiredSelect = false; + } else { + this.requiredSelect = "condition"; + } + }, reset() { //deep copy so it doesn't update during edits if (this.modelValue) { @@ -518,6 +541,7 @@ export default { if (this.column.refSchema != undefined) { this.loadRefSchema(); } + this.setupRequiredSelect(); this.modalVisible = false; }, isEditable(column) { diff --git a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java index 96784cf8ff..8bdf91422c 100644 --- a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java +++ b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/graphql/GraphqlSchemaFieldFactory.java @@ -178,7 +178,7 @@ public class GraphqlSchemaFieldFactory { .field( GraphQLFieldDefinition.newFieldDefinition() .name(REQUIRED) - .type(Scalars.GraphQLBoolean)) + .type(Scalars.GraphQLString)) .field( GraphQLFieldDefinition.newFieldDefinition() .name(DEFAULT_VALUE) @@ -369,7 +369,7 @@ public class GraphqlSchemaFieldFactory { .field( GraphQLInputObjectField.newInputObjectField() .name(REQUIRED) - .type(Scalars.GraphQLBoolean)) + .type(Scalars.GraphQLString)) .field( GraphQLInputObjectField.newInputObjectField() .name(DEFAULT_VALUE) diff --git a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/json/Column.java b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/json/Column.java index 38ba20df3a..5c1a088082 100644 --- a/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/json/Column.java +++ b/backend/molgenis-emx2-graphql/src/main/java/org/molgenis/emx2/json/Column.java @@ -16,7 +16,7 @@ public class Column { private boolean drop = false; // needed in case of migrations private String oldName; private Integer key = 0; - private Boolean required = false; + private String required = null; private Boolean readonly = false; private String defaultValue; private String refSchemaId = null; @@ -87,7 +87,7 @@ public Column(org.molgenis.emx2.Column column, TableMetadata table, boolean mini this.refLabelDefault = column.getRefLabelDefault(); // this.cascadeDelete = column.isCascadeDelete(); this.validation = column.getValidation(); - this.required = column.isRequired(); + this.setRequired(column.getRequired()); this.readonly = column.isReadonly(); this.defaultValue = column.getDefaultValue(); this.descriptions = @@ -160,14 +160,22 @@ public void setKey(Integer key) { this.key = key; } - public Boolean getRequired() { - return required; + public boolean isRequired() { + return required != null && required.equals("true"); } public void setRequired(Boolean required) { + this.required = required.toString(); + } + + public void setRequired(String required) { this.required = required; } + public String getRequired() { + return this.required; + } + public String getRefTableId() { return refTableId; } diff --git a/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx2/Emx2.java b/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx2/Emx2.java index 9c2aa0d395..34fa4be626 100644 --- a/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx2/Emx2.java +++ b/backend/molgenis-emx2-io/src/main/java/org/molgenis/emx2/io/emx2/Emx2.java @@ -118,7 +118,7 @@ public static SchemaMetadata fromRowList(Iterable rows) { if (r.notNull(REF_TABLE)) column.setRefTable(r.getString(REF_TABLE)); if (r.notNull(REF_LINK)) column.setRefLink(r.getString(REF_LINK)); if (r.notNull(REF_BACK)) column.setRefBack(r.getString(REF_BACK)); - if (r.notNull(REQUIRED)) column.setRequired(r.getBoolean(REQUIRED)); + if (r.notNull(REQUIRED)) column.setRequired(r.getString(REQUIRED)); if (r.notNull(DEFAULT_VALUE)) column.setDefaultValue(r.getString(DEFAULT_VALUE)); if (r.notNull(DESCRIPTION)) column.setDescription(r.getString(DESCRIPTION)); // description i18n @@ -269,7 +269,7 @@ public static List toRowList(SchemaMetadata schema) { row.setString(COLUMN_NAME, c.getName()); if (!c.getColumnType().equals(STRING)) row.setString(COLUMN_TYPE, c.getColumnType().toString().toLowerCase()); - if (c.isRequired()) row.setBool(REQUIRED, c.isRequired()); + if (c.getRequired() != null) row.setString(REQUIRED, c.getRequired()); if (c.getDefaultValue() != null) row.setString(DEFAULT_VALUE, c.getDefaultValue()); if (c.getKey() > 0) row.setInt(KEY, c.getKey()); if (c.getRefSchemaName() != null && !c.getRefSchemaName().equals(c.getSchemaName())) diff --git a/backend/molgenis-emx2-io/src/test/java/org/molgenis/emx2/io/TestImportEmptyAutoId.java b/backend/molgenis-emx2-io/src/test/java/org/molgenis/emx2/io/TestImportEmptyAutoId.java index f93819558f..00cff64b56 100644 --- a/backend/molgenis-emx2-io/src/test/java/org/molgenis/emx2/io/TestImportEmptyAutoId.java +++ b/backend/molgenis-emx2-io/src/test/java/org/molgenis/emx2/io/TestImportEmptyAutoId.java @@ -1,57 +1,58 @@ package org.molgenis.emx2.io; +import static org.junit.jupiter.api.Assertions.*; +import static org.molgenis.emx2.Column.column; +import static org.molgenis.emx2.TableMetadata.table; + +import java.io.File; +import java.util.List; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.molgenis.emx2.*; import org.molgenis.emx2.sql.TestDatabaseFactory; -import java.io.File; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.molgenis.emx2.Column.column; -import static org.molgenis.emx2.TableMetadata.table; - @Tag("slow") public class TestImportEmptyAutoId { - static Database db; - private static final String SCHEMA_NAME = "TestImportAutoId"; - private static final String TABLE_NAME = "TestImportAutoIdTable"; - private static final String ID_FIELD_NAME = "testTableId"; - private static final String TEST_XLS_FILENAME = "TestImportEmptyAutoId.xlsx"; - static Schema schema; - - @BeforeAll - public static void setup() { - db = TestDatabaseFactory.getTestDatabase(); - schema = db.dropCreateSchema(SCHEMA_NAME); - createTable(); - } - - @Test - public void testImportEmptyAutoIdFieldXls() { - ClassLoader classLoader = getClass().getClassLoader(); - File xlsFile = new File(classLoader.getResource(TEST_XLS_FILENAME).getFile()); - - MolgenisIO.importFromExcelFile(xlsFile.toPath(), schema, true); - - List rows = schema.getTable(TABLE_NAME).retrieveRows(); - - assertFalse(rows.isEmpty()); - assertNotNull(rows.get(0).getString(ID_FIELD_NAME)); - - db.dropSchemaIfExists(SCHEMA_NAME); - } - - static void createTable() { - schema.create( - table( - TABLE_NAME, - column(ID_FIELD_NAME).setPkey().setType(ColumnType.AUTO_ID).setComputed("TEST:${mg_autoid}"), - column("testStringField").setType(ColumnType.STRING), - column("testIntField").setType(ColumnType.INT))); - } - + static Database db; + private static final String SCHEMA_NAME = "TestImportAutoId"; + private static final String TABLE_NAME = "TestImportAutoIdTable"; + private static final String ID_FIELD_NAME = "testTableId"; + private static final String TEST_XLS_FILENAME = "TestImportEmptyAutoId.xlsx"; + static Schema schema; + + @BeforeAll + public static void setup() { + db = TestDatabaseFactory.getTestDatabase(); + schema = db.dropCreateSchema(SCHEMA_NAME); + createTable(); + } + + @Test + public void testImportEmptyAutoIdFieldXls() { + ClassLoader classLoader = getClass().getClassLoader(); + File xlsFile = new File(classLoader.getResource(TEST_XLS_FILENAME).getFile()); + + MolgenisIO.importFromExcelFile(xlsFile.toPath(), schema, true); + + List rows = schema.getTable(TABLE_NAME).retrieveRows(); + + assertFalse(rows.isEmpty()); + assertNotNull(rows.get(0).getString(ID_FIELD_NAME)); + + db.dropSchemaIfExists(SCHEMA_NAME); + } + + static void createTable() { + schema.create( + table( + TABLE_NAME, + column(ID_FIELD_NAME) + .setPkey() + .setType(ColumnType.AUTO_ID) + .setComputed("TEST:${mg_autoid}"), + column("testStringField").setType(ColumnType.STRING), + column("testIntField").setType(ColumnType.INT))); + } } diff --git a/backend/molgenis-emx2-sql-it/src/test/java/org/molgenis/emx2/sql/TestConditionalRequired.java b/backend/molgenis-emx2-sql-it/src/test/java/org/molgenis/emx2/sql/TestConditionalRequired.java new file mode 100644 index 0000000000..ddb91e9512 --- /dev/null +++ b/backend/molgenis-emx2-sql-it/src/test/java/org/molgenis/emx2/sql/TestConditionalRequired.java @@ -0,0 +1,106 @@ +package org.molgenis.emx2.sql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.molgenis.emx2.TableMetadata.table; + +import org.junit.jupiter.api.Test; +import org.molgenis.emx2.*; + +public class TestConditionalRequired { + + @Test + public void testConditionallyRequiredOnSingleFieldInt() { + String expression = "age > 5"; + + TableMetadata tableMetadata = + table( + "Test", + new Column("age").setType(ColumnType.INT), + new Column("status").setType(ColumnType.STRING).setRequired(expression)); + + Row validRow = new Row("age", 4, "status", null); + + SqlTypeUtils.applyValidationAndComputed(tableMetadata.getColumns(), validRow); // success + + Row invalidRow = validRow.set("age", 6); + + assertThrows( + MolgenisException.class, + () -> { + SqlTypeUtils.applyValidationAndComputed(tableMetadata.getColumns(), invalidRow); + }); + } + + @Test + public void testConditionallyRequiredOnSingleField() { + String requiredExpression = "if(field_one) 'if field_one is provided field_two is required'"; + + TableMetadata tableMetadata = + table( + "Test", + new Column("field_one").setType(ColumnType.STRING), + new Column("field_two").setType(ColumnType.STRING).setRequired(requiredExpression)); + Row validRow = + new Row( + "field_one", "provided", + "field_two", "provided"); + + SqlTypeUtils.applyValidationAndComputed(tableMetadata.getColumns(), validRow); // success + + Row invalidRow = new Row("field_one", "provided", "field_two", null); + + Exception exception = + assertThrows( + MolgenisException.class, + () -> { + SqlTypeUtils.applyValidationAndComputed(tableMetadata.getColumns(), invalidRow); + }); + + assertEquals( + exception.getMessage(), + "column 'field_two' is required: " + + "if field_one is provided field_two is required in ROW(field_one='provided' field_two='null' )"); + } + + @Test + public void testConditionallyRequiredOnMultipleFields() { + String requiredExpression = + "if(onMedication && " + + "age > 10 &&" + + "weight > 3 &&" + + "species === 'cat') 'Medical status should be provided when an old fat cat is on drugs'"; + + TableMetadata tableMetadata = + table( + "Pet", + new Column("age").setType(ColumnType.INT), + new Column("weight").setType(ColumnType.DECIMAL), + new Column("species").setType(ColumnType.STRING), + new Column("onMedication").setType(ColumnType.BOOL), + new Column("medicalStatus").setType(ColumnType.STRING).setRequired(requiredExpression)); + + Row invalidRow = + new Row( + "age", "12", + "weight", "4", + "species", "cat", + "onMedication", true); + + Exception exception = + assertThrows( + MolgenisException.class, + () -> { + SqlTypeUtils.applyValidationAndComputed(tableMetadata.getColumns(), invalidRow); + }); + + assertEquals( + exception.getMessage(), + "column 'medicalStatus' is required: " + + "Medical status should be provided when an old fat cat is on drugs " + + "in ROW(age='12' weight='4' species='cat' onMedication='true' )"); + + Row validRow = invalidRow.set("medicalStatus", "old and fat"); + SqlTypeUtils.applyValidationAndComputed(tableMetadata.getColumns(), validRow); // success + } +} diff --git a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/MetadataUtils.java b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/MetadataUtils.java index e10392093b..38f08b1d8d 100644 --- a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/MetadataUtils.java +++ b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/MetadataUtils.java @@ -72,8 +72,8 @@ public class MetadataUtils { field(name("columnProfiles"), VARCHAR.nullable(true).getArrayType()); private static final Field COLUMN_TYPE = field(name("columnType"), VARCHAR.nullable(false)); - private static final Field COLUMN_REQUIRED = - field(name("required"), BOOLEAN.nullable(false)); + private static final Field COLUMN_REQUIRED = + field(name("required"), VARCHAR.nullable(true)); private static final Field COLUMN_REF_TABLE = field(name("ref_table"), VARCHAR.nullable(true)); private static final Field COLUMN_REF_SCHEMA = @@ -520,7 +520,7 @@ protected static void saveColumnMetadata(DSLContext jooq, Column column) { Objects.toString(column.getColumnType(), null), column.getKey(), column.getPosition(), - column.isRequired(), + column.getRequired(), refSchema, column.getRefTableName(), column.getRefLink(), @@ -542,7 +542,7 @@ protected static void saveColumnMetadata(DSLContext jooq, Column column) { .set(COLUMN_TYPE, Objects.toString(column.getColumnType(), null)) .set(COLUMN_KEY, column.getKey()) .set(COLUMN_POSITION, column.getPosition()) - .set(COLUMN_REQUIRED, column.isRequired()) + .set(COLUMN_REQUIRED, column.getRequired()) .set(COLUMN_REF_SCHEMA, refSchema) .set(COLUMN_REF_TABLE, column.getRefTableName()) .set(COLUMN_REF_LINK, column.getRefLink()) @@ -578,7 +578,7 @@ private static Column recordToColumn(org.jooq.Record col) { Column c = new Column(col.get(COLUMN_NAME, String.class)); c.setLabels(col.get(COLUMN_LABEL) != null ? col.get(COLUMN_LABEL, Map.class) : new TreeMap<>()); c.setType(ColumnType.valueOf(col.get(COLUMN_TYPE, String.class))); - c.setRequired(col.get(COLUMN_REQUIRED, Boolean.class)); + c.setRequired(col.get(COLUMN_REQUIRED, String.class)); c.setKey(col.get(COLUMN_KEY, Integer.class)); c.setPosition(col.get(COLUMN_POSITION, Integer.class)); c.setRefSchemaName(col.get(COLUMN_REF_SCHEMA, String.class)); diff --git a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/Migrations.java b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/Migrations.java index d98a4fba4c..b7834ab31a 100644 --- a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/Migrations.java +++ b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/Migrations.java @@ -20,7 +20,7 @@ public class Migrations { // version the current software needs to work - private static final int SOFTWARE_DATABASE_VERSION = 14; + private static final int SOFTWARE_DATABASE_VERSION = 15; private static Logger logger = LoggerFactory.getLogger(Migrations.class); public static synchronized void initOrMigrate(SqlDatabase db) { @@ -96,6 +96,10 @@ public static synchronized void initOrMigrate(SqlDatabase db) { executeMigrationFile(tdb, "migration14.sql", "cleanup column_metadata ref column"); } + if (version < 15) { + executeMigrationFile(tdb, "migration15.sql", "changed required field to varchar"); + } + // if success, update version to SOFTWARE_DATABASE_VERSION updateDatabaseVersion((SqlDatabase) tdb, SOFTWARE_DATABASE_VERSION); }); diff --git a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java index 82aff015cf..2df262bca7 100644 --- a/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java +++ b/backend/molgenis-emx2-sql/src/main/java/org/molgenis/emx2/sql/SqlTypeUtils.java @@ -62,7 +62,7 @@ public static void applyValidationAndComputed(List columns, Row row) { } else if (c.getComputed() != null) { row.set(c.getName(), executeJavascriptOnMap(c.getComputed(), graph)); } else if (columnIsVisible(c, graph)) { - checkRequired(c, row); + checkRequired(c, row, graph); checkValidation(c, graph); } else { if (c.isReference()) { @@ -89,24 +89,31 @@ private static void applyAutoid(Column c, Row row) { } } - private static void checkRequired(Column c, Row row) { - if (!row.isDraft() - && c.getComputed() == null - && !AUTO_ID.equals(c.getColumnType()) - && c.isRequired()) { - - if (c.isReference()) { - for (Reference r : c.getReferences()) { - if (row.isNull(r.getName(), r.getPrimitiveType())) { - throw new MolgenisException("column '" + c.getName() + "' is required in " + row); - } + private static void checkRequired(Column c, Row row, Map values) { + if (!row.isDraft() && c.getComputed() == null && !AUTO_ID.equals(c.getColumnType())) { + if (c.isRequired() && hasEmptyFields(c, row)) { + throw new MolgenisException("column '" + c.getName() + "' is required in " + row); + } else if (c.isConditionallyRequired()) { + String error = checkRequiredExpression(c.getRequired(), values); + if (error != null && hasEmptyFields(c, row)) { + throw new MolgenisException( + "column '" + c.getName() + "' is required: " + error + " in " + row); } - } else { - if (row.isNull(c.getName(), c.getColumnType())) { - throw new MolgenisException("column '" + c.getName() + "' is required in " + row); + } + } + } + + private static boolean hasEmptyFields(Column c, Row row) { + if (c.isReference()) { + for (Reference r : c.getReferences()) { + if (row.isNull(r.getName(), r.getPrimitiveType())) { + return true; } } + } else { + return row.isNull(c.getName(), c.getColumnType()); } + return false; } private static boolean columnIsVisible(Column column, Map values) { @@ -213,6 +220,21 @@ public static String checkValidation(String validationScript, Map values) { + try { + Object error = executeJavascriptOnMap(validationScript, values); + if (error instanceof Boolean) { + if ((Boolean) error) return validationScript; + return null; + } + if (error != null) return error.toString(); + } catch (MolgenisException me) { + throw me; + } + return null; + } + static Map convertRowToMap(List columns, Row row) { Map map = new LinkedHashMap<>(); for (Column c : columns) { diff --git a/backend/molgenis-emx2-sql/src/main/resources/org/molgenis/emx2/sql/migration15.sql b/backend/molgenis-emx2-sql/src/main/resources/org/molgenis/emx2/sql/migration15.sql new file mode 100644 index 0000000000..b2a551610e --- /dev/null +++ b/backend/molgenis-emx2-sql/src/main/resources/org/molgenis/emx2/sql/migration15.sql @@ -0,0 +1,2 @@ +ALTER TABLE "MOLGENIS"."column_metadata" + ALTER COLUMN required type varchar; \ No newline at end of file diff --git a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Column.java b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Column.java index 380db36e70..dcb26ebbbe 100644 --- a/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Column.java +++ b/backend/molgenis-emx2/src/main/java/org/molgenis/emx2/Column.java @@ -37,7 +37,7 @@ public class Column extends HasLabelsDescriptionsAndSettings implements null; // column order within the table. During import/export these may change private int key = 0; // 1 is primary key 2..n is secondary keys - private boolean required = false; + private String required = null; private String validation = null; private String visible = null; // javascript expression to influence vibility private String computed = null; // javascript expression to compute a value, overrides updates @@ -249,14 +249,27 @@ public Column setReadonly(Boolean readonly) { } public boolean isRequired() { - return required; + return required != null && required.equalsIgnoreCase("true"); } - public Column setRequired(boolean required) { + public Column setRequired(Boolean required) { + this.required = required.toString(); + return this; + } + + public Column setRequired(String required) { this.required = required; return this; } + public String getRequired() { + return this.required; + } + + public boolean isConditionallyRequired() { + return !isRequired() && getRequired() != null && !"false".equalsIgnoreCase(getRequired()); + } + public Boolean isCascadeDelete() { return cascadeDelete; } diff --git a/docs/molgenis/use_schema.md b/docs/molgenis/use_schema.md index ca75c8fa05..cfd9dcef6e 100644 --- a/docs/molgenis/use_schema.md +++ b/docs/molgenis/use_schema.md @@ -93,6 +93,29 @@ is used as the primary key in the user interface, upload and API. Other key>1 ca When required=TRUE then values in this column must be filled. When required=FALSE then this column can be left empty. Default value: FALSE. +Besides TRUE and FALSE, it is also possible to provide a JavaScript expression using conditional logic based on the values of other columns. + +For instance, consider a table with 'name' and 'surname' fields. If the 'name' field is provided, the 'surname' field should also be populated. The expression for the surname field could look like: +`if(name!=null) 'Surname should be provided when name is not null'` + +The string after the expression is the validation message returned when the expression yields `true`, and the field is not provided. + +It's also possible to use an expression without an error message: +`name != null`. +Will return the expression as an error message. + + +Expressions can also reference multiple fields. For example, consider a 'pet' table with the following fields: +- `species` (string) +- `onMedication` (bool) +- `age` (int) +- `weight` (decimal) +- `medicalStatus` (string) + +The following expression would make 'medicalStatus' required if a cat is old, heavy, and on medication: +`if(onMedication && age > 10 && weight > 3 && species === 'cat') +'Medical status should be provided when an old fat cat is on drugs'` + ### defaultValue Using 'defaultValue' you can set a default value for a column. In forms, this value will be pre-filled.