4. Write the Model Code

How to Encode a JavaScript Data Model

The JavaScript data model can be directly encoded for getting the code of the model layer of our JavaScript frontend app.

4.1. Summary

  1. Encode each model class as a JavaScript constructor function.

  2. Encode the property checks in the form of class-level ('static') methods. Take care that all constraints of a property as specified in the JavaScript data model are properly encoded in the property checks.

  3. Encode the property setters as (instance-level) methods. In each setter, the corresponding property check is invoked and the property is only set, if the check does not detect any constraint violation.

  4. Encode any other operation.

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

4.2. Encode each class of the JavaScript data model as a constructor function

Each class C of the data model is encoded by means of a corresponding JavaScript constructor function with the same name C having a single parameter slots, which has a key-value slot providing a value for each non-derived property of the class. The range of these properties should be indicated in a comment. In the case of a reference property the range is another model class.

In the constructor body, we first assign default values to all properties. The default value of a reference property is null. These default values will be used when the constructor is invoked as a default constructor, that is, without any argument. If the constructor is invoked with arguments, the default values may be overwritten by calling the setter methods for all properties.

For instance, the Publisher class from the JavaScript data model is encoded in the following way:

function Publisher( slots) {
  // set the default values for the parameter-free default constructor
  this.name = "";     // String
  this.address = "";  // String
  // constructor invocation with arguments
  if (arguments.length > 0) {
    this.setName( slots.name); 
    this.setAddress( slots.address); 
  }
};

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

For each model class C, we define a class-level property C.instances representing the collection of all C instances managed by the application in the form of an entity table (a map of records): This property is initially set to {}. For instance, in the case of the model class Book, we define:

Publisher.instances = {};

The Book class from the JavaScript data model is encoded in a similar way:

function Book( slots) {
  // set the default values for the parameter-free default constructor
  this.isbn = "";         // string
  this.title = "";        // string
  this.year = 0;          // number(int)
  this.publisher = null;  // Publisher
  // constructor invocation with a slots argument
  if (arguments.length > 0) {
    this.setIsbn( slots.isbn); 
    this.setTitle( slots.title); 
    this.setYear( slots.year);
    if (slots.publisher) this.setPublisher( slots.publisher);
    else if (slots.publisherIdRef) this.setPublisher( slots.publisherIdRef);  }
}

Notice that the Book constructor can be invoked either with an object reference slots.publisher or with an ID reference slots.publisherIdRef. This liberal approach makes using the Book constructor more flexible and more robust.

4.3. Encode the property checks

Take care that all constraints of a property as specified in the JavaScript data model are properly encoded in its check function, as explained in Part 2 (Validation Tutorial). Error classes are defined in the file lib/errorTypes.js.

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

Publisher.checkName = function (n) {
  if (!n) {
    return new NoConstraintViolation();  
  } else if (typeof(n) !== "string" || n.trim() === "") {
    return new TypeConstraintViolation(
        "The name must be a non-empty string!");
  } else {
    return new NoConstraintViolation();
  }
};

Notice that, since the name attribute is the standard ID attribute of Publisher, we only check syntactic constraints in checkName, and check the mandatory value and uniqueness constraints in checkNameAsId, which invokes checkName:

Publisher.checkNameAsId = function (n) {
  var constraintViolation = Publisher.checkName( n);
  if ((constraintViolation instanceof NoConstraintViolation)) {
    if (!n) {
      return new MandatoryValueConstraintViolation(
          "A value for the name must be provided!");
    } else if (Publisher.instances[n]) {  
      constraintViolation = new UniquenessConstraintViolation(
          "There is already a publisher record with this name!");
    } else {
      constraintViolation = new NoConstraintViolation();
    } 
  }
  return constraintViolation;
};

Since for any standard ID attribute, we may have to deal with ID references (foreign keys) in other classes, we need to provide a further check function, called checkNameAsIdRef, for checking the referential integrity constraint, as illustrated in the following example:

Publisher.checkNameAsIdRef = function (n) {
  var constraintViolation = Publisher.checkName( n);
  if ((constraintViolation instanceof NoConstraintViolation)) {
    if (!Publisher.instances[n]) {  
      constraintViolation = new ReferentialIntegrityConstraintViolation(
          "There is no publisher record with this name!");
    } 
  }
  return constraintViolation;
};

The condition (!Publisher.instances[n]) checks if there is no publisher object with the given name n, and then creates a constraintViolation object. This referential integrity constraint check is used by the following Book.checkPublisher function:

Book.checkPublisher = function (publisherIdRef) {
  var constraintViolation = null;
  if (!publisherIdRef) {
    constraintViolation = new NoConstraintViolation();  // optional
  } else {
    // invoke foreign key constraint check
    constraintViolation = Publisher.checkNameAsIdRef( publisherIdRef);
  }
  return constraintViolation;
};

4.4. Encode the property setters

Encode the setter operations 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. In the case of a reference property, we allow invoking the setter either with an internal object reference or with an ID reference. The resulting ambiguity is resolved by testing if the argument provided in the invocation of the setter is an object or not. For instance, the setter operation setPublisher is encoded in the following way:

Book.prototype.setPublisher = function (p) {
  var constraintViolation = null;
  var publisherIdRef = "";
  // a publisher can be given as ...
  if (typeof(p) !== "object") {  // an ID reference or 
    publisherIdRef = p;
  } else {                       // an object reference
    publisherIdRef = p.name;
  }
  constraintViolation = Book.checkPublisher( publisherIdRef);
  if (constraintViolation instanceof NoConstraintViolation) {
    // create the new publisher reference 
    this.publisher = Publisher.instances[ publisherIdRef];
  } else {
    throw constraintViolation;
  }
};

4.5. Implement a deletion policy

For any reference property, we have to choose and implement one of the two possible deletion policies discussed in 2 for managing the corresponding object destruction dependency in the destroy method of the property's range class. In our case, we have to choose between

  1. deleting all books published by the deleted publisher;

  2. dropping from all books published by the deleted publisher the reference to the deleted publisher.

We go for the second option. This is shown in the following code of the Publisher.destroy method where for all concerned book objects book the property book.publisher is cleared:

Publisher.destroy = function (name) {
  var publisher = Publisher.instances[name];
  var book=null, keys=[];
  // delete all references to this publisher in book objects
  keys = Object.keys( Book.instances);
  for (var i=0; i < keys.length; i++) {
    book = Book.instances[keys[i]];
    if (book.publisher === publisher) delete book.publisher;
  }
  // delete the publisher record
  delete Publisher.instances[name];
  console.log("Publisher " + name + " deleted.");
};

4.6. Serialization and De-Serialization

The serialization method convertObj2Row converts typed objects with internal object references to corresponding (untyped) record objects with ID references:

Book.prototype.convertObj2Row = function () {
  var bookRow = util.cloneObject(this), keys=[];
  if (this.publisher) {
    // create publisher ID reference
    bookRow.publisherIdRef = this.publisher.name;
  }
  return bookRow;
};

The de-serialization method convertRow2Obj converts (untyped) record objects with ID references to corresponding typed objects with internal object references:

Book.convertRow2Obj = function (bookRow) {
  var book={}, persKey="";
  var publisher = Publisher.instances[bookRow.publisherIdRef];
  // replace the publisher ID reference with object reference
  delete bookRow.publisherIdRef;
  bookRow.publisher = publisher;
  try {
    book = new Book( bookRow);
  } catch (e) {
    console.log( e.constructor.name + " while deserializing a book row: " + e.message);
  }
  return book;
};