Skip to content

Commit

Permalink
feat: Required expression so required can be conditional (#3152)
Browse files Browse the repository at this point in the history
* Added "null" check when checking for duplicate keys

* Added check if primary key values are supplied.

* Added test reading xls file without ids

* Created a schema instead of using "pet store"

* Refactor column required field to String

* Added migration script and bug fix

* added required check at front-end

* Added required conditional check to back-end

* Added conditionalRequired function to column

* Added front-end test

* Bug fix.

* Formatting

* Formatting

* Again formatting

* Added migrate sql script to migrate class

* extracted required validation from checkRequired

* Added unit tests

* Refactored method name

* Added documentation.

* Fix for regular expression without a message

* Typo

* Updated tests
  • Loading branch information
harmbrugge authored Jan 10, 2024
1 parent b685020 commit 71a5fb1
Show file tree
Hide file tree
Showing 15 changed files with 374 additions and 85 deletions.
2 changes: 1 addition & 1 deletion apps/meta-data-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface IColumn {
refLinkId?: string;
refSchemaId?: string;
refTableId?: string;
required?: boolean;
required?: string;
semantics?: string[];
validation?: string;
visible?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("getRowErrors", () => {
id: "required",
label: "required",
columnType: "STRING",
required: true,
required: "true",
},
],
} as ITableMetaData;
Expand All @@ -55,14 +55,70 @@ describe("getRowErrors", () => {
id: "required",
label: "required",
columnType: "DECIMAL",
required: true,
required: "true",
},
],
} as ITableMetaData;
const result = getRowErrors(metaData, rowData);
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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -82,6 +94,24 @@ function isInValidNumericValue(columnType: string, value: number) {
}
}

function getRequiredExpressionError(
expression: string,
values: Record<string, any>,
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<string, any>,
Expand Down
30 changes: 27 additions & 3 deletions apps/schema/src/components/ColumnEditModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,18 @@
</div>
<div class="row">
<div class="col-4" v-if="isEditable(column)">
<InputBoolean
id="column_required"
v-model="column.required"
<InputRadio
id="column_required_radio"
label="required"
:options="[true, false, 'condition']"
v-model="requiredSelect"
description="Will give error unless field is filled in. Is not checked if not visible"
@update:modelValue="setRequired"
/>
<InputString
id="column_required"
v-model="column.required"
v-if="requiredSelect === 'condition'"
/>
</div>
<div class="col-4" v-if="isEditable(column)">
Expand Down Expand Up @@ -269,6 +276,7 @@ import {
InputString,
InputText,
InputTextLocalized,
InputRadio,
LayoutForm,
LayoutModal,
MessageError,
Expand Down Expand Up @@ -296,6 +304,7 @@ export default {
InputString,
InputBoolean,
InputSelect,
InputRadio,
IconAction,
InputTextLocalized,
LayoutModal,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -472,6 +482,9 @@ export default {
this.reset();
this.modalVisible = false;
},
setRequired() {
this.column.required = this.requiredSelect;
},
refLinkCandidates() {
return this.table.columns
.filter(
Expand Down Expand Up @@ -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) {
Expand All @@ -518,6 +541,7 @@ export default {
if (this.column.refSchema != undefined) {
this.loadRefSchema();
}
this.setupRequiredSelect();
this.modalVisible = false;
},
isEditable(column) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public class GraphqlSchemaFieldFactory {
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name(REQUIRED)
.type(Scalars.GraphQLBoolean))
.type(Scalars.GraphQLString))
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name(DEFAULT_VALUE)
Expand Down Expand Up @@ -369,7 +369,7 @@ public class GraphqlSchemaFieldFactory {
.field(
GraphQLInputObjectField.newInputObjectField()
.name(REQUIRED)
.type(Scalars.GraphQLBoolean))
.type(Scalars.GraphQLString))
.field(
GraphQLInputObjectField.newInputObjectField()
.name(DEFAULT_VALUE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public static SchemaMetadata fromRowList(Iterable<Row> 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
Expand Down Expand Up @@ -269,7 +269,7 @@ public static List<Row> 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()))
Expand Down
Loading

0 comments on commit 71a5fb1

Please sign in to comment.