6. Write the Model Code

How to Encode a JavaScript Data Model

The JavaScript data model shown on the right hand side in Figure 6.2 can be encoded 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. Encode the model class as a JavaScript constructor function.

  2. Encode 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 JavaScript data model, are properly encoded in the check functions.

  3. Encode 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. Encode the add and remove operations, if there are any, as instance-level methods.

  5. Encode any other operation.

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

6.2. Encode the model class as a constructor function

The class Book is encoded by means of a corresponding JavaScript 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)
  if (arguments.length > 0) {
    this.setIsbn( slots.isbn); 
    this.setTitle( slots.title); 
    this.setYear( slots.year);
    if (slots.edition) {  // optional
      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 data types of the information design model to the corresponding implicit JavaScript data types according to the following table.

Table 6.2. Datatype mapping

Platform-independent datatype JavaScript datatype
String string
Integer number (int)
Decimal number (float)
Boolean boolean
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. Encode the property checks

Encode the property check functions in the form of class-level ('static') methods. In JavaScript, this means to define them as function slots of the constructor, as in Book.checkIsbn. Take care that all constraints of a property as specified in the data model are properly encoded in its check function. This concerns, in particular, the mandatory value and uniqueness constraints implied by the standard identifier declaration (with «stdid»), 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.3 and defined in the file model/errorTypes.js 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;
};

6.4. Encode the property setters

Encode 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 encoded in the following way:

Book.prototype.setIsbn = function (id) {
  var 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 () {
  return "Book{ ISBN:" + this.isbn + ", title:" + 
      this.title + ", year:" + this.year +"}"; 
};

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.convertRow2Obj and Book.loadAll 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 example 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 procedures Book.convertRow2Obj, Book.add, Book.update and Book.createTestData; and

  2. 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 in line 6. Otherwise, the new book object is added to Book.instances and a status message is created in lines 10 and 11, 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!");
  }
};

Likewise, when a constraint violation is detected in one of the setters invoked in Book.update, a constraint violation error message is created (in line 16) and the previous state of the object is restored (in line 19). Otherwise, a status message is created (in lines 23 or 25), as shown in the following program listing:

Book.update = function (slots) {
  var book = Book.instances[slots.isbn],
      noConstraintViolated = true,
      updatedProperties = [],
      objectBeforeUpdate = util.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 && book.edition !== parseInt(slots.edition)) {
      book.setEdition( slots.edition);
      updatedProperties.push("edition");
    }
  } 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 + " !");
    }
  }
};