4. Code the Model

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

4.1. Summary

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

  1. Code the property check functions 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 get and set methods 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 functions toString() and toJSON().

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

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

4.2. Code each model class as a JS class

Each class C of the JS class model is coded as a JS 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 can 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;  // string
    this.address = address;  // string
  }
  ...
};

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 an empty map {}. 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;  // string
    this.title = title;  // string
    this.year = year;  // integer
    this.authors = authors || authorIdRefs;  // Array
    if (publisher || publisher_id) {
      this.publisher = publisher || publisher_id;  // ref|string
    }
  }
  ...
}

Notice that the Book constructor can be invoked either with object references authors and publisher or with ID references authorIdRefs and publisher_id (the type hint "ref|string" means that the property's range is either an object reference or a string). This approach makes using the Book constructor more flexible and more robust.

4.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 5 . Recall that constraint violation (or validation error) classes are defined in the module file lib/errorTypes.mjs.

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) {
      return new MandatoryValueConstraintViolation(
          "A publisher name is required!");
    } else if (Publisher.instances[n]) {
      return UniquenessConstraintViolation(
          "There is already a publisher record with this name!");
    }
  } 
  return validationResult;
}

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 Publisher.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;
  }
  ...
}

4.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;
      }
    }
  }
  ...
}

4.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 record of a publisher p, we have to choose between

  1. deleting all records of books published by p (Existential Dependency);

  2. dropping the reference to p from all books published by p (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 (by deleting its property-value slot):

Publisher.destroy = function (name) {
  // delete all references to this publisher in book objects
  for (const key of Object.keys( Book.instances)) {
    const book = Book.instances[key];
    if (book.publisher.name === name) {
      delete book._publisher;  // delete the proporty-value slot
    }
  }
  // delete the publisher object
  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.

4.6. Serialization functions

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}, authors: ${Object.keys( this.authors).join(",")} }`;
}

The object-to-storage conversion function Book::toJSON(), which is automatically invoked by the built-in JSON.stringify function, converts typed JS objects with object references to corresponding (untyped) record objects with ID references. This includes deleting the underscore prefix for obtaining the corresponding record field name:

toJSON() {
  var rec = {};
  for (const 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;
    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.