5. Code the Model

The JS class model can be directly coded for getting the JS classes of our app.

5.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. Write the code of the serialization function toString() and the storage conversion function toRecord().

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

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

5.2. Code each model class as an ES2015 class

Each class C of the JS class model is coded as an ES2015 class with the same name C and a constructor having a single record parameter, which specifies a field 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 assign the fields of the record parameter to corresponding properties. These property assignments invoke the corresponding setter methods.

For instance, the Publisher class from the JS class model is coded in the following way:

class Publisher {
  constructor ({name, address}) {
    this.name = name;
    this.address = 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 Publisher, we define:

Publisher.instances = {};

The Book class from the JS class model is coded in a similar way:

class Book {
  constructor ({isbn, title, year, authors, authorIdRefs, 
      publisher, publisher_id}) {
    this.isbn = isbn;
    this.title = title;
    this.year = year;
    this.authors = authors || authorIdRefs;
    if (publisher || publisher_id) {
      this.publisher = publisher || publisher_id;
    }
  }
  ...
}

Notice that the Book constructor can be invoked either with object references authors and publisher or with ID references authorIdRefs and publisher_id. This approach makes using the Book constructor more flexible and more robust.

5.3. Code the property checks

Take care that all constraints of a property as specified in the JS class model are properly coded in its check function, as explained in Chapter 8 of Volume 1. Recall that constraint violation (or validation error) classes are defined in the file lib/errorTypes.js.

For instance, for the Publisher.checkName function we obtain the following code:

class Publisher {
  ...
  static checkName(n) {
    if (n === undefined) {
      return new NoConstraintViolation();  // not mandatory
    } else {
      if (typeof n !== "string" || n.trim() === "") {
        return new RangeConstraintViolation(
		    "The name must be a non-empty string!");
      } else {
        return new NoConstraintViolation();
      }
    }
  }
  static checkNameAsId(n) {...}
  ...
}

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:

static checkNameAsId( n) {
  var validationResult = Publisher.checkName(n);
  if ((validationResult instanceof NoConstraintViolation)) {
    if (n === undefined) {
      return new MandatoryValueConstraintViolation(
          "A publisher name is required!");
    } else if (Publisher.instances[n]) { 
      validationResult = new UniquenessConstraintViolation(
          "There is already a publisher record with this name!");
    } else {
      validationResult = new NoConstraintViolation();
    }
  }
}

If we 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:

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

The condition !Publisher.instances[n] checks if there is no publisher object with the given name n, and then creates a validationResult object as an instance of the exception class ReferentialIntegrityConstraintViolation. The Book.checkNameAsIdRef function is called by the Book.checkPublisher function:

class Book {
  ...
  static checkPublisher( publisher_id) {
    var validationResult = null;
    if (publisher_id === undefined || publisher_id === "") {
      validationResult = new NoConstraintViolation();  // optional
    } else {
      // invoke foreign key constraint check
      validationResult = Publisher.checkNameAsIdRef( publisher_id);
    }
    return validationResult;
  }
  ...
}

5.4. Code the property setters

In the setters, the corresponding check function is called 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 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 publisher setter is coded in the following way:

class Book {
  ...
  set publisher( p) {
    if (!p) {  // unset publisher
      delete this._publisher;
    } else {
      // p can be an ID reference or an object reference
      const publisher_id = (typeof p !== "object") ? p : p.name;
      const validationResult = Book.checkPublisher( publisher_id);
      if (validationResult instanceof NoConstraintViolation) {
        // create the new publisher reference
        this._publisher = Publisher.instances[ publisher_id];
      } else {
        throw validationResult;
      }
    }
  }
  ...
}

5.5. Choose and implement a deletion policy

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

  1. deleting all records of books published by the deleted publisher (Existential Dependency);

  2. dropping from all books published by the deleted publisher the reference to the deleted publisher (Existential Independence).

Assuming that books do not existentially depend on their publishers, we choose the second option. This is shown in the following code of the Publisher.destroy method where for all concerned book objects the property book.publisher is cleared:

Publisher.destroy = function (name) {
  const keys = Object.keys( Book.instances);
  var publisher = Publisher.instances[name];
  // delete all references to this publisher in book objects
  for (let i=0; i < keys.length; i++) {
    let 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.");
};

Notice that the deletion of all references to the deleted publisher is performed in a sequential scan through all book objects, which may be inefficient when there are many of them. It would be much more efficient when each publisher object would hold a list of references to all books published by this publisher. Creating and maintaining such a list would make the association between books and their publisher bidirectional.

5.6. Serialization and Object-to-Storage Mapping

In the case of a reference property, like Book::publisher, the serialization function Book::toString() has to show a human-readable identifier of the referenced object, like this.publisher.name:

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

The object-to-storage conversion function Book::toRecord() converts typed JS objects with object references to corresponding (untyped) record objects with ID references:

toRecord() {
  var rec = {};
  for (let p of Object.keys( this)) {
    // copy only property slots with underscore prefix
    if (p.charAt(0) === "_") {
      switch (p) {
      case "_publisher":
        // convert object reference to ID reference
        if (this._publisher) rec.publisher_id = this._publisher.name;
        break;
      default:
        // remove underscore prefix
        rec[p.substr(1)] = this[p];
      }
    }
  };
  return rec;
}

The inverse conversion, from untyped record objects with ID references to corresponding typed objects with object references, is performed by the Book constructor, which tolerates both ID references and object references as arguments for setting reference properties.