Copyright © 2023 Gerd Wagner (CC BY-NC)
You may run the example app from our server.
Published 2023-07-25
This introductory tutorial presents the Object Event Modeling JavaScript framework OEMjs, which allows building low-code web apps based on business object, business event and business activity classes defined with property ranges and other property constraints. OEMjs allows building apps quickly without boilerplate code for constraint validation, data storage management and user interfaces.
For setting up a CRUD data management app with OEMjs, you have to write
app
object that imports the app's business object classes and includes a definition of test data;For all three types of files, you may use a copy from one of the example apps (available at the OEMjs GitHub repo) as your model or starting point. In this tutorial, we use the "Book Data Management" app from the apps/minimal folder.
While you can directly run an OEMjs app from a remote website (e.g., from the OEMjs GitHub website) or from a local web server, you can only run it from your local file system after changing your browser's default configuration. For FireFox, you have to set the configuration property
privacy.file_unique_origin
to false by enteringabout:config
in the browser's web address bar.
The "Book Data Management" app has only one class module, Book.mjs, which defines the business object class Book (with seven properties) in the following way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import bUSINESSoBJECT from "../../../src/bUSINESSoBJECT.mjs"; import {dt} from "../../../src/datatypes.mjs"; class Book extends bUSINESSoBJECT { constructor ({isbn, title, year, edition, purchaseDate, recordCreatedOn=new Date(), isReserved=false}) { super( isbn); this.title = title; this.year = year; if (edition) this.edition = edition; this.purchaseDate = purchaseDate; this.recordCreatedOn = recordCreatedOn; this.isReserved = isReserved; } } Book.properties = { "isbn": {range:"String", label:"ISBN", ...}, "title": {range:"NonEmptyString", label:"Title", ...}, "year": {range:"Integer", label:"Year", ...}, "edition": {range:"PositiveInteger", label:"Edition", ...}, "purchaseDate": {range:"Date", label:"Purchase date", ...}, "recordCreatedOn": {range:"DateTime", label:"Record created on", ...}, "isReserved": {range: "Boolean", label:"Is reserved"} } Book.displayAttribute = "title"; // collect business object classes in a map dt.classes["Book"] = Book; export default Book; |
For any other business object class, the same code sections are needed. In particular, you need to define your business object classes by extending the pre-defined OEMjs class bUSINESSoBJECT
, which defines an ID attribute, implying that in the constructor of the subclass you need to invoke super
with the ID constructor parameter of the subclass. In the example code above, since the ID attribute of the Book class is "isbn", we have the invocation super(isbn)
.
Notice that the properties of a class are not only defined in the constructor function (where they are assigned), but also in the property definitions map Book.properties
which includes a definition record for each property, defining a range and a label for them. The property definitions are not fully shown above, indicated by "...", which stands for the missing property constraint definitions. A complete discussion of property constraints is provided in the next section.
Suitable range constraints can be defined by using one of the supported range keywords listed below.
In addition to the ordinary alphanumeric attributes "isbn", "title", "year" and "edition", which are rendered in the UI in the form of input fields, the Book class has three attributes with special ranges:
For the simple "Book Data Management" app in the folder apps/minimal/, the js/app.mjs module has the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // import the app's business object classes import Book from "./Book.mjs"; // import the required framework classes import sTORAGEmANAGER from "../../../src/storage/sTORAGEmANAGER.mjs"; import bUSINESSaPP from "../../../src/bUSINESSaPP.mjs"; // create the app object const app = new bUSINESSaPP({title:"Minimal OEMjs App", storageManager: new sTORAGEmANAGER({adapterName:"IndexedDB", dbName:"MinApp", createLog: true, validateBeforeSave: true}) }); |
For any other app, the same code sections are needed in app.mjs. Notice that in the creation of the business app object in lines 7-9, a storage manager is specified with an adapterName parameter set to "IndexedDB", which refers to the browser's built-in local storage technology. The IndexedDB storage option is always available and does not require any special setup. In addition to "IndexedDB", OEMjs will also support remote storage technologies, especially cloud storage technologies such as Google's FireStore or Cloudflare's D1.
Any web application is started by loading a web page that loads its code and provides its user interface. It is common to use the name "index.html" for this app start page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta charset="UTF-8"/> ... <script type="module"> import vIEW from "../../src/ui/vIEW.mjs"; import app from "./js/app.mjs"; window.addEventListener("load", vIEW.setupUI( app) ); </script> </head> <body> ... </body> </html> |
The HTML head
element contains a script
element, in which first the framework library vIEW.mjs and then the app.mjs module described above is loaded, followed by adding an event listener that calls the user interface setup procedure vIEW.setupUI
after the HTML document has been loaded.
For detecting non-admissible and inconsistent data and for preventing such data to be added to an application's database, we need to define suitable integrity constraints that can be used by the application's data validation mechanisms for catching these cases of flawed data. Integrity constraints are logical conditions that must be satisfied by the data entered by a user and stored in the application's database.
For instance, if an application is managing data about persons including their birth dates and their death dates, then we must make sure that for any person record with a death date, this date is not before that person's birth date.
Since integrity maintenance is fundamental in database management, the data definition language part of the relational database language SQL supports the definition of integrity constraints in various forms. On the other hand, however, there is hardly any support for integrity constraints and data validation in common programming languages such as Python, Java, C# or JavaScript. It is therefore important to take a systematic approach to constraint validation in web application engineering, like choosing an application development framework that provides sufficient support for it.
Unfortunately, many web application development frameworks do not provide sufficient support for defining integrity constraints and performing data validation. Integrity constraints should be defined in one (central) place in an app, and then be used for configuring the user interface and for validating data in different parts of the app, such as in the user interface code and in the data storage code. In terms of usability, the goals should be:
To prevent the user from entering invalid data in the user interface (UI) by limiting the input options, if possible.
To detect and reject invalid user input as early as possible by performing constraint validation in the UI for those UI widgets where invalid user input cannot be prevented by limiting the input options.
To prevent that invalid data pollutes the app's main memory state and persistent database state by performing constraint validation also in the model layer and in the data storage layer.
HTML5 provides support for validating user input in an HTML-forms-based user interface (UI). Here, the goal is to provide immediate feedback to the user whenever invalid data has been entered into a form field. This UI mechanism of responsive validation is an important feature of modern web applications. In traditional web applications, the back-end component validates the data and returns the validation results in the form of a set of error messages to the front-end. Only then, often several seconds later, and in the hard-to-digest form of a bulk message, does the user get the validation feedback.
Integrity constraints (or simply constraints) are logical conditions on the data of an app. They may take many different forms. The most important type of constraints, property constraints, define conditions on the admissible property values of an object. They are defined for an object type (or class) such that they apply to all objects of that type. Some of the most important cases of property constraints are:
require that an attribute must have a value from the value space of the type that has been defined as its range. For instance, in the following definition of the property "title", its range is defined as "NonEmptyString", which means that its values must be non-empty strings:
Book.properties = { "title": {range:"NonEmptyString", label:"Title"}, }
require that the length of a string value for an attribute is less than a certain maximum number, or greater than a minimum number. String length constraints are defined with the fields min
and max
in a property definition:
Book.properties = { "title": {range:"NonEmptyString", label:"Title", min: 2, max: 50}, }
require that a property must have a value. For instance, a book must have a title, so the "title" attribute is defined to be mandatory In OEMjs, all properties are mandatory by default. A property is only non-mandatory, if it is defined to be optional, as in the following example:
Book.properties = { "edition": {range:"PositiveInteger", label:"Edition", optional: true}, }
require that the value of a numeric attribute must be in a specific interval, which defined with the fields min
and max
:
Book.properties = { "year": {range:"Integer", label:"Year", min: 1459, max: () => (new Date()).getFullYear()+1}, }
Notice that in this example, the value of the max
field is a JS function that computes the next year.
require that a string attribute's value must match a certain pattern defined by a regular expression. The pattern is defined with the field pattern
, and the error message to be displayed in the case of a pattern violation is defined with the field patternMessage
:
Book.properties = { "isbn": {range:"String", label:"ISBN", pattern:/\b\d{9}(\d|X)\b/, patternMessage:"The ISBN must be a 9-digit string followed by 'X'!"}, }
apply to multi-valued properties, only, and require that the cardinality of a multi-valued property's value set is not less than a given minimum cardinality or not greater than a given maximum cardinality.
Person.properties = { "phoneNumbers": {range:"NonEmptyString", label:"Phone numbers", minCard: 1, maxCard: Infinity}, }
Using the special value Infinity
as the value of the maxCard
field means that the size of the property's value set is unbounded.
require that a property's value is mandatory and unique among all instances of the given object type. They are defined with the help of the field isIdAttribute
:
Book.properties = { "isbn": {range:"String", isIdAttribute: true, label:"ISBN"}, }
Based on the property definitions of the Book.mjs module file, a default user interface (UI) is generated for each of the data management operations Create, Retrieve, Update and Delete (CRUD).
For the data management operation Retrieve, the following UI is created, showing all book records retrieved from the app's database:
Notice that since the property Edition is optional, some book records have no value in the corresponding column. The values of the property Purchase date are shown using a localized format (here, the German date format). The values of the property Record created on are time stamps (or date-time values). The values of the Boolean property Is reserved are shown in the yes/no format.
For the data management operation Update, the following UI is created:
Integrity constraints (or simply constraints) are logical conditions on the data of an app. They may take many different forms. The most important type of constraints, property constraints, define conditions on the admissible property values of an object. They are defined for an object type (or class) such that they apply to all objects of that type. We concentrate on the most important cases of property constraints:
require that the length of a string value for an attribute is less than a certain maximum number, or greater than a minimum number.
require that a property must have a value. For instance, a person must have a name, so the name attribute must not be empty.
require that an attribute must have a value from the value space of the type that has been defined as its range. For instance, an integer attribute must not have the value "aaa".
require that the value of a numeric attribute must be in a specific interval.
require that a string attribute's value must match a certain pattern defined by a regular expression.
apply to multi-valued properties, only, and require that the cardinality of a multi-valued property's value set is not less than a given minimum cardinality or not greater than a given maximum cardinality.
require that a property's value is unique among all instances of the given object type.
require that the values of a reference property refer to an existing object in the range of the reference property.
require that the value of a property must not be changed after it has been assigned initially.
The visual language of UML class diagrams supports defining integrity constraints either in a special way for special cases (like with predefined keywords), or, in the general case, with the help of invariants, which are conditions expressed either in plain English or in the Object Constraint Language (OCL) and shown in a special type of rectangle attached to the model element concerned. We use UML class diagrams for modeling constraints in design models that are independent of a specific programming language or technology platform.
UML class diagrams provide special support for expressing multiplicity (or cardinality) constraints. This type of constraint allows to specify a lower multiplicity (minimum cardinality) or an upper multiplicity (maximum cardinality), or both, for a property or an association end. In UML, this takes the form of a multiplicity expression l..u
where the lower multiplicity l
is a non-negative integer and the upper multiplicity u
is either a positive integer not smaller than l
or the special value *
standing for unbounded. For showing property multiplicity (or cardinality) constrains in a class diagram, multiplicity expressions are enclosed in brackets and appended to the property name, as shown in the Person
class rectangle below.
In the following sections, we discuss the different types of property constraints listed above in more detail. We also show how to express some of them in computational languages such as UML class diagrams, SQL table creation statements, JavaScript model class definitions, or the annotation-based languages Java Bean Validation annotations and ASP.NET Data Annotations.
Any systematic approach to constraint validation also requires to define a set of error (or 'exception') classes, including one for each of the standard property constraints listed above.
The length of a string value for a property such as the title of a book may have to be constrained, typically rather by a maximum length, but possibly also by a minimum length. In an SQL table definition, a maximum string length can be specified in parenthesis appended to the SQL datatype CHAR
or VARCHAR
, as in VARCHAR(50)
.
UML does not define any special way of expressing string length constraints in class diagrams. Of course, we always have the option to use an invariant for expressing any kind of constraint, but it seems preferable to use a simpler form of expressing these property constraints. One option is to append a maximum length, or both a minimum and a maximum length, in parenthesis to the datatype name, like so
Another option is to use min/max constraint keywords in the property modifier list:
A mandatory value constraint requires that a property must have a value. This can be expressed in a UML class diagram with the help of a multiplicity constraint expression where the lower multiplicity is 1. For a single-valued property, this would result in the multiplicity expression 1..1
, or the simplified expression 1
, appended to the property name in brackets. For example, the following class diagram defines a mandatory value constraint for the property name
:
Whenever a class rectangle does not show a multiplicity expression for a property, the property is mandatory (and single-valued), that is, the multiplicity expression 1
is the default for properties.
In an SQL table creation statement, a mandatory value constraint is expressed in a table column definition by appending the key phrase NOT NULL
to the column definition as in the following example:
CREATE TABLE persons( name VARCHAR(30) NOT NULL, age INTEGER )
According to this table definition, any row of the persons
table must have a value in the column name
, but not necessarily in the column age
.
In JavaScript, we can code a mandatory value constraint by a class-level check function that tests if the provided argument evaluates to a value, as illustrated in the following example:
Person.checkName = function (n) { if (n === undefined) { return "A name must be provided!"; // constraint violation error message } else return ""; // no constraint violation };
With Java Bean Validation, a mandatory property like name
is annotated with NotNull
in the following way:
@Entity public class Person { @NotNull private String name; private int age; }
The equivalent ASP.NET Data Annotation is Required
as shown in
public class Person { [Required] public string name { get; set; } public int age { get; set; } }
A range constraint requires that a property must have a value from the value space of the type that has been defined as its range. This is implicitly expressed by defining a type for a property as its range. For instance, the attribute age
defined for the object type Person
in the class diagram above has the range Integer
, so it must not have a value like "aaa", which does not denote an integer. However, it may have values like -13 or 321, which also do not make sense as the age of a person. In a similar way, since its range is String
, the attribute name
may have the value "" (the empty string), which is a valid string that does not make sense as a name.
We can avoid allowing negative integers like -13 as age values, and the empty string as a name, by assigning more specific datatypes as range to these attributes, such as NonNegativeInteger
to age
, and NonEmptyString
to name
. Notice that such more specific datatypes are neither predefined in SQL nor in common programming languages, so we have to implement them either in the form of user-defined types, as supported in SQL-99 database management systems such as PostgreSQL, or by using suitable additional constraints such as interval constraints, which are discussed in the next section. In a UML class diagram, we can simply define NonNegativeInteger
and NonEmptyString
as custom datatypes and then use them in the definition of a property, as illustrated in the following diagram:
In JavaScript, we can code a range constraint by a check function, as illustrated in the following example:
Person.checkName = function (n) { if (typeof(n) !== "string" || n.trim() === "") { return "Name must be a non-empty string!"; } else return ""; };
This check function detects and reports a constraint violation if the given value for the name
property is not of type "string" or is an empty string.
In a Java EE web app, for declaring empty strings as non-admissible user input we must set the context parameter javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL
to true
in the web deployment descriptor file web.xml
.
In ASP.NET, empty strings are non-admissible by default.
An interval constraint requires that an attribute's value must be in a specific interval, which is specified by a minimum value or a maximum value, or both. Such a constraint can be defined for any attribute having an ordered type, but normally we define them only for numeric datatypes or calendar datatypes. For instance, we may want to define an interval constraint requiring that the age
attribute value must be in the interval [25,70]. In a class diagram, we can define such a constraint by using the property modifiers min
and max
, as shown for the age
attribute of the Driver
class in the following diagram.
In an SQL table creation statement, an interval constraint is expressed in a table column definition by appending a suitable CHECK
clause to the column definition as in the following example:
CREATE TABLE drivers( name VARCHAR NOT NULL, age INTEGER CHECK (age >= 25 AND age <= 70) )
In JavaScript, we can code an interval constraint in the following way:
Driver.checkAge = function (a) { if (a < 25 || a > 70) { return "Age must be between 25 and 70!"; } else return ""; };
In Java Bean Validation, we express this interval constraint by adding the annotations Min()
and Max()
to the property age
in the following way:
@Entity public class Driver { @NotNull private String name; @Min(25) @Max(70) private int age; }
The equivalent ASP.NET Data Annotation is Range(25,70)
as shown in
public class Driver{ [Required] public string name { get; set; } [Range(25,70)] public int age { get; set; } }
A pattern constraint requires that a string attribute's value must match a certain pattern, typically defined by a regular expression. For instance, for the object type Book
we define an isbn
attribute with the datatype String
as its range and add a pattern constraint requiring that the isbn
attribute value must be a 10-digit string or a 9-digit string followed by "X" to the Book
class rectangle shown in the following diagram.
In an SQL table creation statement, a pattern constraint is expressed in a table column definition by appending a suitable CHECK
clause to the column definition as in the following example:
CREATE TABLE books( isbn VARCHAR(10) NOT NULL CHECK (isbn ~ '^\d{9}(\d|X)$'), title VARCHAR(50) NOT NULL )
The ~
(tilde) symbol denotes the regular expression matching predicate and the regular expression ^\d{9}(\d|X)$
follows the syntax of the POSIX standard (see, e.g. the PostgreSQL documentation).
In JavaScript, we can code a pattern constraint by using the built-in regular expression function test
, as illustrated in the following example:
Person.checkIsbn = function (id) { if (!/\b\d{9}(\d|X)\b/.test( id)) { return "The ISBN must be a 10-digit string or a 9-digit string followed by 'X'!"; } else return ""; };
In Java EE Bean Validation, this pattern constraint for isbn
is expressed with the annotation Pattern
in the following way:
@Entity public class Book { @NotNull @Pattern(regexp="^\\(\d{9}(\d|X))$") private String isbn; @NotNull private String title; }
The equivalent ASP.NET Data Annotation is RegularExpression
as shown in
public class Book{ [Required] [RegularExpression(@"^(\d{9}(\d|X))$")] public string isbn { get; set; } public string title { get; set; } }
A cardinality constraint requires that the cardinality of a multi-valued property's value set is not less than a given minimum cardinality or not greater than a given maximum cardinality. In UML, cardinality constraints are called multiplicity constraints, and minimum and maximum cardinalities are expressed with the lower bound and the upper bound of the multiplicity expression, as shown in the following diagram, which contains two examples of properties with cardinality constraints.
The attribute definition nickNames[0..3]
in the class Person
specifies a minimum cardinality of 0 and a maximum cardinality of 3, with the meaning that a person may have no nickname or at most 3 nicknames. The reference property definition members[3..5]
in the class Team
specifies a minimum cardinality of 3 and a maximum cardinality of 5, with the meaning that a team must have at least 3 and at most 5 members.
It's not obvious how cardinality constraints could be checked in an SQL database, as there is no explicit concept of cardinality constraints in SQL, and the generic form of constraint expressions in SQL, assertions, are not supported by available DBMSs. However, it seems that the best way to implement a minimum (or maximum) cardinality constraint is an on-delete (or on-insert) trigger that tests the number of rows with the same reference as the deleted (or inserted) row.
In JavaScript, we can code a cardinality constraint validation for a multi-valued property by testing the size of the property's value set, as illustrated in the following example:
Person.checkNickNames = function (nickNames) { if (nickNames.length > 3) { return "There must be no more than 3 nicknames!"; } else return ""; };
With Java Bean Validation annotations, we can specify
@Size( max=3) List<String> nickNames @Size( min=3, max=5) List<Person> members
A uniqueness constraint (or key constraint) requires that a property's value (or the value list of a list of properties in the case of a composite key constraint) is unique among all instances of the given object type. For instance, in a UML class diagram with the object type Book
we can define the isbn
attribute to be unique, or, in other words, a key, by appending the (user-defined) property modifier keyword key
in curly braces to the attribute's definition in the Book
class rectangle shown in the following diagram.
In an SQL table creation statement, a uniqueness constraint is expressed by appending the keyword UNIQUE
to the column definition as in the following example:
CREATE TABLE books( isbn VARCHAR(10) NOT NULL UNIQUE, title VARCHAR(50) NOT NULL )
In JavaScript, we can code this uniqueness constraint by a check function that tests if there is already a book with the given isbn
value in the books
table of the app's database.
A unique attribute (or a composite key) can be declared to be the standard identifier for objects of a given type, if it is mandatory (or if all attributes of the composite key are mandatory). We can indicate this in a UML class diagram with the help of the property modifier id
appended to the declaration of the attribute isbn
as shown in the following diagram.
Notice that such a standard ID declaration implies both a mandatory value and a uniqueness constraint on the attribute concerned.
Often, practitioners do not recommended using a composite key as a standard ID, since composite identifiers are more difficult to handle and not always supported by tools. Whenever an object type does not have a key attribute, but only a composite key, it may therefore be preferable to add an artificial standard ID attribute (also called surrogate ID) to the object type. However, each additional surrogate ID has a price: it creates some cognitive and computational overhead. Consequently, in the case of a simple composite key, it may be preferable not to add a surrogate ID, but use the composite key as the standard ID.
There is also an argument against using any real attribute, such as the isbn
attribute, for a standard ID. The argument points to the risk that the values even of natural ID attributes like isbn
may have to be changed during the life time of a business object, and any such change would require an unmanageable effort to change also all corresponding ID references. However, the business semantics of natural ID attributes implies that they are frozen. Thus, the need of a value change can only occur in the case of a data input error. But such a case is normally detected early in the life time of the object concerned, and at this stage the change of all corresponding ID references is still manageable.
Standard IDs are called primary keys in relational databases. We can declare an attribute to be the primary key in an SQL table creation statement by appending the phrase PRIMARY KEY
to the column definition as in the following example:
CREATE TABLE books( isbn VARCHAR(10) PRIMARY KEY, title VARCHAR(50) NOT NULL )
In object-oriented programming languages, like JavaScript and Java, we cannot code a standard ID declaration, because this would have to be part of the metadata of a class definition, and there is no support for such metadata. However, we should still check the implied mandatory value and uniqueness constraints.
A referential integrity constraint requires that the values of a reference property refer to an object that exists in the population of the property's range class. Since we do not deal with reference properties in this chapter, we postpone the discussion of referential integrity constraints to Part 4 of our tutorial.
A frozen value constraint defined for a property requires that the value of this property must not be changed after it has been assigned. This includes the special case of read-only value constraints on mandatory properties that are initialized at object creation time.
Typical examples of properties with a frozen value constraint are standard identifier attributes and event properties. In the case of events, the semantic principle that the past cannot be changed prohibits that the property values of events can be changed. In the case of a standard identifier attribute we may want to prevent users from changing the ID of an object since this requires that all references to this object using the old ID value are changed as well, which may be difficult to achieve (even though SQL provides special support for such ID changes by means of its ON UPDATE CASCADE
clause for the change management of foreign keys).
The following diagram shows how to define a frozen value constraint for the isbn
attribute:
In Java, a read-only value constraint can be enforced by declaring the property to be final
. In JavaScript, a read-only property slot can be implemented as in the following example:
Object.defineProperty( obj, "teamSize", {value: 5, writable: false, enumerable: true})
where the property slot obj.teamSize
is made unwritable. An entire object obj
can be frozen with Object.freeze( obj)
.
We can implement a frozen value constraint for a property in the property's setter method like so:
Book.prototype.setIsbn = function (i) { if (this.isbn === undefined) this.isbn = i; else console.log("Attempt to re-assign a frozen property!"); }
So far, we have only discussed how to define and check property constraints. However, in certain cases there may be also integrity constraints that do not just depend on the value of a particular property, but rather on
the values of several properties of a particular object (object-level constraints),
the value of a property before and its value after a change attempt (dynamic constraints),
the set of all instances of a particular object type (type-level constraints),
the set of all instances of several object types.
In a class model, property constraints can be expressed within the property declaration line in a class rectangle (typically with keywords, such as id
, max
, etc.). For expressing more complex constraints, such as object-level or type-level constraints, we can attach an invariant declaration box to the class rectangle(s) concerned and express the constraint either in (unambiguous) English or in the Object Constraint Language (OCL). A simple example of an object-level constraint expressed as an OCL invariant is shown in Figure A-1. An example of an object-level constraint.
A general approach for implementing object-level constraint validation consists of taking the following steps:
Choose a fixed name for an object-level constraint validation function, such as validate
.
For any class that needs object-level constraint validation, define a validate
function returning either a ConstraintViolation
or a NoConstraintViolation
object.
Call this function, if it exists, for the given model class,
in the UI/view, on form submission;
in the model class, before save, both in the create
and in the update
method.
This problem is well-known from classical web applications where the front-end component submits the user input data via HTML form submission to a back-end component running on a remote web server. Only this back-end component validates the data and returns the validation results in the form of a set of error messages to the front-end. Only then, often several seconds later, and in the hard-to-digest form of a bulk message, does the user get the validation feedback. This approach is no longer considered acceptable today. Rather, in a responsive validation approach, the user should get immediate validation feedback on each single data input. Technically, this can be achieved with the help of event handlers for the user interface events input
or change
.
Responsive validation requires a data validation mechanism in the user interface (UI), such as the HTML5 form validation API, which essentially provides new types of input
fields (such as number
or date
), a set of new attributes for form control elements and new JS methods for the purpose of supporting responsive validation performed by the browser. Since using the new validation attributes (like required
, min
, max
and pattern
) implies defining constraints in the UI, they are not really useful in a general approach where constraints are only checked, but not defined, in the UI.
Consequently, we only use two methods of the HTML5 form validation API for validating constraints in the HTML-forms-based user interface of our app. The first of them, setCustomValidity
, allows to mark a form field as either valid or invalid by assigning either an empty string or a non-empty (constraint violation) message string.
The second method, checkValidity
, is invoked on a form before user input data is committed or saved (for instance with a form submission). It tests, if all fields have a valid value. For having the browser automatically displaying any constraint violation messages, we need to have a submit
event, even if we don't really submit the form, but just use a save
button.
See also this Mozilla tutorial for more about the HTML5 form validation API.
Integrity constraints should be defined in the model classes of an MVC app since they are part of the business semantics of a model class (representing a business object type). However, a more difficult question is where to perform data validation? In the database? In the model classes? In the controller? Or in the user interface ("view")? Or in all of them?
A relational database management system (DBMS) performs data validation whenever there is an attempt to change data in the database, provided that all relevant integrity constraints have been defined in the database. This is essential since we want to avoid, under all circumstances, that invalid data enters the database. However, it requires that we somehow duplicate the code of each integrity constraint, because we want to have it also in the model class to which the constraint belongs.
Also, if the DBMS would be the only application component that validates the data, this would create a latency, and hence usability, problem in distributed applications because the user would not get immediate feedback on invalid input data. Consequently, data validation needs to start in the user interface (UI).
However, it is not sufficient to perform data validation in the UI. We also need to do it in the model classes, and in the database, for making sure that no flawed data enters the application's persistent data store. This creates the problem of how to maintain the constraint definitions in one place (the model), but use them in two or three other places (at least in the model classes and in the UI code, and possibly also in the database).We call this the multiple validation problem. This problem can be solved in different ways. For instance:
Define the constraints in a declarative language (such as Java Bean Validation Annotations or ASP.NET Data Annotations) and generate the back-end/model and front-end/UI validation code both in a back-end application programming language such as Java or C#, and in JavaScript.
Keep your validation functions in the (PHP, Java, C# etc.) model classes on the back-end, and invoke them from the JavaScript UI code via XHR. This approach can only be used for specific validations, since it implies the penalty of an additional HTTP communication latency for each validation invoked in this way.
Use JavaScript as your back-end application programming language (such as with NodeJS), then you can code your validation functions in your JavaScript model classes on the back-end and execute them both before committing changes on the back-end and on user input and form submission in the UI on the front-end side.
The simplest, and most responsive, solution is the third one, using only JavaScript both for the back-end and front-end components of a web app.
The data integrity rules (or 'business rules') that govern the management of data can be expressed in the form of invariants in a UML class model as shown in the following diagram.
In this model, the following constraints have been expressed:
Due to the fact that the isbn
attribute is declared to be the standard identifier of Book
, it is mandatory and unique.
The isbn
attribute has a pattern constraint requiring its values to match the ISBN-10 format that admits only 10-digit strings or 9-digit strings followed by "X".
The title
attribute is mandatory, as indicated by its multiplicity expression [1], and has a string length constraint requiring its values to have at most 50 characters.
The year
attribute is mandatory and has an interval constraint, however, of a special form since the maximum is not fixed, but provided by the calendar function nextYear()
, which we implement as a utility function.
Notice that the edition
attribute is not mandatory, but optional, as indicated by its multiplicity expression [0..1]. In addition to the constraints described in this list, there are the implicit range constraints defined by assigning the datatype NonEmptyString
as range to isbn
and title
, Integer
to year
, and PositiveInteger
to edition
. In our plain JavaScript approach, all these property constraints are coded in the model class within property-specific check functions.
The meaning of the design model can be illustrated by a sample data population respecting all constraints:
ISBN | Title | Year | Edition |
---|---|---|---|
006251587X | Weaving the Web | 2000 | 3 |
0465026567 | Gödel, Escher, Bach | 1999 | 2 |
0465030793 | I Am A Strange Loop | 2008 |