6. Write the Model Code

The JS class model shown on the right hand side in Figure 5.1 can be coded step by step for getting the code of the model layer of our JavaScript front-end app. These steps are summarized in the following section.

6.1. Summary

  1. Code the model class as a JavaScript constructor function.

  2. Code the check functions, such as checkIsbn or checkTitle, in the form of class-level ('static') methods. Take care that all constraints, as specified in the JS class model, are properly coded in the check functions.

  3. Code the setter operations, such as setIsbn or setTitle, as (instance-level) methods. In the setter, the corresponding check operation is invoked and the property is only set, if the check does not detect any constraint violation.

  4. Code the add and remove operations, if there are any, as instance-level methods.

These steps are discussed in more detail in the following sections.

6.2. Code the model class as a constructor function

The class Book is coded as a corresponding constructor function with the same name Book such that all its (non-derived) properties are supplied with values from corresponding key-value slots of a slots parameter.

function Book( slots) {
  // assign default values
  this.isbn = "";   // string
  this.title = "";  // string
  this.year = 0;    // number (int)
  // assign properties only if the constructor is invoked with an argument
  if (arguments.length > 0) {
    this.setIsbn( slots.isbn); 
    this.setTitle( slots.title); 
    this.setYear( slots.year);
    // optional property
    if (slots.edition) this.setEdition( slots.edition);
  }
};

In the constructor body, we first assign default values to the class properties. These values will be used when the constructor is invoked as a default constructor (without arguments), or when it is invoked with only some arguments. It is helpful to indicate the range of a property in a comment. This requires to map the platform-independent datatypes of the information design model to the corresponding implicit JavaScript datatypes according to the following table.

Table 5.1. Datatype mapping

Platform-independent datatype JavaScript datatype SQL
String string CHAR(n) or VARCHAR(n)
Integer number (int) INTEGER
Decimal number (float) REAL, DOUBLE PRECISION or DECIMAL(p,s)
Boolean boolean BOOLEAN
Date Date DATE

Since the setters may throw constraint violation errors, the constructor function, and any setter, should be called in a try-catch block where the catch clause takes care of processing errors (at least logging suitable error messages).

As in the minimal app, we add a class-level property Book.instances representing the collection of all Book instances managed by the app in the form of an entity table:

Book.instances = {};

6.3. Code the property checks

Code the property check functions in the form of class-level ('static') methods. In JavaScript, this means to define them as method slots of the constructor, as in Book.checkIsbn (recall that a constructor is a JS object, since in JavaScript, functions are objects, and as an object, it can have slots).

Take care that all constraints of a property as specified in the class model are properly coded in its check function. This concerns, in particular, the mandatory value and uniqueness constraints implied by the standard identifier declaration (with {id}), and the mandatory value constraints for all properties with multiplicity 1, which is the default when no multiplicity is shown. If any constraint is violated, an error object instantiating one of the error classes listed above in Section 5.1 and defined in the file errorTypes.mjs is returned.

For instance, for the checkIsbn operation we obtain the following code:

Book.checkIsbn = function (id) {
  if (!id) {
    return new NoConstraintViolation();
  } else if (typeof id !== "string" || id.trim() === "") {
    return new RangeConstraintViolation(
        "The ISBN must be a non-empty string!");
  } else if (!/\b\d{9}(\d|X)\b/.test( id)) {
    return new PatternConstraintViolation(
        "The ISBN must be a 10-digit string or "+
        " a 9-digit string followed by 'X'!");
  } else {
    return new NoConstraintViolation();
  }
};

Notice that, since isbn is the standard identifier attribute of Book, we only check the syntactic constraints in checkIsbn, but we check the mandatory value and uniqueness constraints in checkIsbnAsId, which itself first invokes checkIsbn:

Book.checkIsbnAsId = function (id) {
  var constraintViolation = Book.checkIsbn( id);
  if ((constraintViolation instanceof NoConstraintViolation)) {
    if (!id) {
      constraintViolation = new MandatoryValueConstraintViolation(
          "A value for the ISBN must be provided!");
    } else if (Book.instances[id]) {  
      constraintViolation = new UniquenessConstraintViolation(
          "There is already a book record with this ISBN!");
    } else {
      constraintViolation = new NoConstraintViolation();
    } 
  }
  return constraintViolation;
};

We assume that all check functions and setters can deal both with proper data values (that are of the attribute's range type) and also with string values that are supposed to represent proper data values, but have not yet been converted to the attribute's range type. We take this approach for avoiding datatype conversions in the user interface ("view") code. Notice that all data entered by a user in an HTML form field is of type String and must be converted (or de-serialized) before its validity can be checked and it can be assigned to the corresponding property. It is preferable to perform these type conversions in the model code, and not in the user interface code..

For instance, in our example app, we have the integer-valued attribute year. When the user has entered a value for this attribute in a corresponding form field, in the Create or Update user interface, the form field holds a string value. This value is passed to the Book.add or Book.update method, which invokes the setYear and checkYear methods. Only after being validated, this string value is converted to an integer and assigned to the year attribute.

6.4. Code the property setters

Code the setter operations as (instance-level) methods. In the setter, the corresponding check function is invoked and the property is only set, if the check does not detect any constraint violation. Otherwise, the constraint violation error object returned by the check function is thrown. For instance, the setIsbn operation is coded in the following way:

Book.prototype.setIsbn = function (id) {
  const validationResult = Book.checkIsbnAsId( id);
  if (validationResult instanceof NoConstraintViolation) {
    this.isbn = id;
  } else {
    throw validationResult;
  }
};

There are similar setters for the other properties (title, year and edition).

6.5. Add a serialization function

It is helpful to have an object serialization function tailored to the structure of an object (as defined by its class) such that the result of serializing an object is a human-readable string representation of the object showing all relevant information items of it. By convention, these functions are called toString(). In the case of the Book class, we use the following code:

Book.prototype.toString = function () {
  var bookStr = `Book{ ISBN: ${this.isbn}, title: ${this.title}, year: ${this.year}`;
  if (this.edition) bookStr += `, edition: ${this.edition}`;
  return bookStr;
};

6.6. Data management operations

In addition to defining the model class in the form of a constructor function with property definitions, checks and setters, as well as a toString() serialization function, we also need to define the following data management operations as class-level methods of the model class:

  1. Book.convertRec2Obj and Book.retrieveAll for loading all managed Book instances from the persistent data store.

  2. Book.saveAll for saving all managed Book instances to the persistent data store.

  3. Book.add for creating a new Book instance and adding it to the collection of all Book instances.

  4. Book.update for updating an existing Book instance.

  5. Book.destroy for deleting a Book instance.

  6. Book.createTestData for creating a few sample book records to be used as test data.

  7. Book.clearData for clearing the book data store.

All of these methods essentially have the same code as in our minimal app discussed in Part 1, except that now

  1. we may have to catch constraint violations in suitable try-catch blocks in the Book procedures convertRec2Obj, add, update and createTestData;

  2. we create more informative status and error log messages for better observing what's going on; and

  3. we can use the toString() function for serializing an object in status and error messages.

Notice that for the change operations add (create) and update, we need to implement an all-or-nothing policy: whenever there is a constraint violation for a property, no new object must be created and no (partial) update of the affected object must be performed.

When a constraint violation is detected in one of the setters called when new Book(...) is invoked in Book.add, the object creation attempt fails, and instead a constraint violation error message is created. Otherwise, the new book object is added to Book.instances and a status message is created, as shown in the following program listing:

Book.add = function (slots) {
  var book = null;
  try {
    book = new Book( slots);
  } catch (e) {
    console.log( `${e.constructor.name}: ${e.message}`);
    book = null;
  }
  if (book) {
    Book.instances[book.isbn] = book;
    console.log( `${book.toString()} created!`);
  }
};

When an object of a model class is to be updated, we first create a clone of it for being able to restore it if the update attempt fails. In the object update attempt, we only assign those properties of the object the value of which has changed, and we report this in a status log.

Normally, all properties defined by a model class, except the standard identifier attribute, can be updated. It is, however, possible to also allow updating the standard identifier attribute. This requires special care for making sure that all references to the given object via its old standard identifier are updated as well.

When a constraint violation is detected in one of the setters invoked in Book.update, the object update attempt fails, and instead the error message of the constraint violation object thrown by the setter and caught in the update method is shown, and the previous state of the object is restored. Otherwise, a status message is created, as shown in the following program listing:

Book.update = function (slots) {
  var noConstraintViolated = true,
      updatedProperties = [];
  const book = Book.instances[slots.isbn],
        objectBeforeUpdate = cloneObject( book);
  try {
    if (book.title !== slots.title) {
      book.setTitle( slots.title);
      updatedProperties.push("title");
    }
    if (book.year !== parseInt( slots.year)) {
      book.setYear( slots.year);
      updatedProperties.push("year");
    }
    if (slots.edition && slots.edition !== book.edition) {
      // slots.edition has a non-empty value that is new
      book.setEdition( slots.edition);
      updatedProperties.push("edition");
    } else if (!slots.edition && book.edition !== undefined) {
      // slots.edition has an empty value that is new
      delete book.edition;  // unset the property "edition"
      updatedProperties.push("edition");
    }
  } catch (e) {...}
  ...
};

Notice that optional properties, like edition, need to be treated in a special way. If the user doesn't enter any value for them in a Create or Update user interface, the form field's value is the empty string "". In the case of an optional property, this means that the property is not assigned a value in the add use case, or that it is unset if it has had a value in the update use case. This is different from the case of a mandatory property, where the empty string value obtained from an empty form field may or may not be an admissible value.

If there is a constraint violation exception, an error message is written to the log and the object concerned is reset to its previous state:

Book.update = function (slots) {
  ...
  try {
    ...
  } catch (e) {
    console.log( e.constructor.name +": "+ e.message);
    noConstraintViolated = false;
    // restore object to its state before updating
    Book.instances[slots.isbn] = objectBeforeUpdate;
  }
  if (noConstraintViolated) {
    if (updatedProperties.length > 0) {
      console.log(`Properties ${updatedProperties.toString()}` + 
          `modified for book ${slots.isbn}`);
    } else {
      console.log(`No property value changed for book ${slots.isbn}!`);
    }
  }
};