4. Code the Model

Notice that, for simplicity, we do not include the code for all constraint validation checks shown in the JS class model in the code of the example app.

4.1. Summary

Code each class of the JS class model as an ES2015 class with implicit getters and setters:

  1. Code the property checks in the form of class-level ('static') methods. Take care that all constraints of a property as specified in the JS class model are properly coded in the property checks.

  2. For each single-valued property, code the specified getter and setter such that in each setter, the corresponding property check is invoked and the property is only set/unset, if the check does not detect any constraint violation.

  3. For each multi-valued property, code its add and remove operations, as well as the specified get/set operations:

    1. Code the add/remove operations as (instance-level) methods that invoke the corresponding property checks.

    2. Code the setter such that it invokes the add operation for each item of the collection to be assigned.

  4. Write the code of the serialization function toString() and the object-to-storage conversion function toRecord().

  5. Take care of deletion dependencies in the destroy method.

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

4.2. Code the add and remove operations

For the multi-valued reference property Book::authors, we need to code the operations addAuthor and removeAuthor. Both operations accept one parameter denoting an author either by ID reference (the author ID as integer or string) or by an object reference. The code of addAuthor is as follows:

addAuthor( a) {
  // a can be an ID reference or an object reference
  const author_id = (typeof a !== "object") ? parseInt( a) : a.authorId;
  const validationResult = Book.checkAuthor( author_id);
  if (author_id && validationResult instanceof NoConstraintViolation) {
    // add the new author reference
    let key = String( author_id);
    this._authors[key] = Author.instances[key];
  } else {
    throw validationResult;
  }
}

In the removeAuthor method, the author reference is first checked and, if no constraint violation is detected, the corresponding entry in the map this._authors is deleted:

removeAuthor( a) {
  // a can be an ID reference or an object reference
  const author_id = (typeof a !== "object") ? 
      parseInt( a) : a.authorId;
  const validationResult = Book.checkAuthor( author_id);
  if (validationResult instanceof NoConstraintViolation) {
    // delete the author reference
    delete this._authors[author_id];
  } else {
    throw validationResult;
  }
}

For assigning an array of ID references, or a map of object references, to the property Book::authors, the method setAuthors adds them one by one with the help of addAuthor:

set authors( a) {
  this._authors = {};
  if (Array.isArray(a)) {  // array of IdRefs
    for (let idRef of a) {
      this.addAuthor( idRef);
    }
  } else {  // map of IdRefs to object references
    for (let idRef of Object.keys( a)) {
      this.addAuthor( a[idRef]);
    }
  }
}

4.3. Choose and implement a deletion policy

For the reference property Book::authors, we have to choose and implement a deletion policy in the destroy method of the Author class. We have to choose between

  1. deleting all books (co-)authored by the deleted author (reflecting the logic of Existential Dependency);

  2. dropping from all books (co-)authored by the deleted author the reference to the deleted author (reflecting the logic of Existential Independence).

For simplicity, we go for the second option. This is shown in the following code of the static Author.destroy method where for all concerned book objects the author reference book.authors[authorKey] is dropped:

Author.destroy = function (authorId) {
  const author = Author.instances[authorId];
  // delete all dependent book records
  for (let isbn of Object.keys( Book.instances)) {
    let book = Book.instances[isbn];
    if (book.authors[authorId]) delete book.authors[authorId];
  }
  // delete the author object
  delete Author.instances[authorId];
  console.log("Author " + author.name + " deleted.");
};

4.4. Serialization and Object-to-Storage Mapping

We need a serialization function toString() for converting an object to a human-readable string representation that can be used for showing an object in a user interface, and an object-to-record mapping function toRecord() converting a typed object to a corresponding record that can be saved in a persistent datastore. In both cases, internal object references are converted to ID references.

The Book::toString() function creates a string representation that may only contain the relevant properties. The simplest method for showing a set of associated objects, like the authors of a book, is creating a comma-separated list of IDs:

toString() {
  var bookStr = `Book{ ISBN: ${this.isbn}, title: ${this.title},` +
      `year: ${this.year}`;
  if (this.publisher) bookStr += `, publisher: ${this.publisher.name}`;
  return bookStr +", authors:" + 
      Object.keys( this.authors).join(",") +"}";
}

The Book::toRecord() function needs to serialize all property slots of an object. This includes deleting the underscore prefix for obtaining the corresponding record field name:

toRecord() {
  var rec = {};
  for (let p of Object.keys( this)) {
    // copy only property slots with underscore prefix
    if (p.charAt(0) !== "_") continue;
    switch (p) {
    case "_publisher":
      // convert object reference to ID reference
      if (this._publisher) rec.publisher_id = this._publisher.name;
      break;
    case "_authors":
      // convert map of object references to list of ID references
      rec.authorIdRefs = [];
      Object.keys( this.authors).forEach( authorIdStr => {
        rec.authorIdRefs.push( parseInt( authorIdStr));
      });
      break;
    default:
      // remove underscore prefix
      rec[p.substr(1)] = this[p];
    }
  }
  return rec;
}