5. Write the Model Code

5.1. Code the enumerations

Three enumerations are coded (within Book.mjs) in the following way with the help of the meta-class Enumeration:

const PublicationFormEL = new Enumeration(
    ["hardcover","paperback","ePub","PDF"]);
const BookCategoryEL = new Enumeration(
    ["novel","biography","textbook","other"]);
const LanguageEL = new Enumeration({"en":"English", 
    "de":"German", "fr":"French", "es":"Spanish"});

Notice that LanguageEL defines a code list enumeration, while PublicationFormEL and BookCategoryEL define simple enumerations.

5.2. Code the model class as a JS class

We want to check if a new property value satisfies all constraints of a property whenever the value of a property is set. A best practice approach for making sure that new values are validated before assigned is using a setter method for assigning a property, and invoking the check in the setter. We can either define an explicit setter method (like setIsbn) for a property (like isbn), or we can use JavaScript's implicit getters and setters in combination with an internal property name (like _isbn). We have used explicit setters in the validation app. Now, in the Book class definition for the enumeration app, we use JavaScript's implicit getters and setters because they offer a more user-friendly syntax and can be conveniently defined in an ES6 (or ES2015) class definition.

The constructor of the class Book is defined with a single record parameter using the ES6 syntax of function parameter destructuring:

class Book {
  constructor ({isbn, title, originalLanguage, otherAvailableLanguages,
                 category, publicationForms}) {
    // assign properties by invoking implicit setters
    this.isbn = isbn;
    this.title = title;
    this.originalLanguage = originalLanguage;
    this.otherAvailableLanguages = otherAvailableLanguages;
    this.category = category;
    this.publicationForms = publicationForms;
  }
  ...
}

Such a constructor is invoked in the following way:

const book = new Book({
    isbn: "006251587X",
    title: "Weaving the Web",
    originalLanguage: LanguageEL.EN,
    otherAvailableLanguages: [LanguageEL.DE, LanguageEL.FR],
    category: BookCategoryEL.NOVEL,
    publicationForms: [
      PublicationFormEL.EPUB,
      PublicationFormEL.PDF
    ]
});

5.3. Code the implicit getters and setters

The internal properties of a class are defined using underscore-prefixed names (like "_isbn"). For each property, we define implicit getters and setters using the predefined JS keywords get and set:

class Book {
  ...
  get isbn() {
    return this._isbn;
  }
  set isbn( n) {
    var validationResult = Book.checkIsbnAsId( n);
    if (validationResult instanceof NoConstraintViolation) {
      this._isbn = n;
    } else {
      throw validationResult;
    }
  }
  ...
}

Notice that the implicit getters and setters access the corresponding internal property, like _isbn. This approach is based on the assumption that this internal property is normally not accessed directly, but only via its getter or setter. Since we can normally assume that developers comply with this rule (and that there is no malicious developer in the team), this approach is normally safe enough. However, there is also a proposal to increase the safety (for avoiding direct access) by generating random names for the internal properties with the help of the ES2015 Symbol class.

5.4. Code the enumeration attribute checks

Code the enumeration attribute checks in the form of class-level ('static') functions that check if the argument is a valid enumeration index not smaller than 1 and not greater than the enumeration's MAX value. For instance, for the checkOriginalLanguage function we obtain the following code:

class Book {
  ...
  static checkOriginalLanguage( ol) {
    if (ol === undefined || ol === "") {
      return new MandatoryValueConstraintViolation(
          "An original language must be provided!");
    } else if (!isIntegerOrIntegerString( ol) ||
        parseInt(ol) < 1 || parseInt(ol) > LanguageEL.MAX) {
      return new RangeConstraintViolation(
          `Invalid value for original language: ${ol}`);
    } else {
      return new NoConstraintViolation();
    }
  }
  ...
}

For a multi-valued enumeration attribute, such as publicationForms, we break down the validation code into two check functions, one for checking if a value is a valid enumeration index (checkPublicationForm), and another one for checking if all members of a set of values are valid enumeration indexes (checkPublicationForms). The first check is coded as follows:

static checkPublicationForm( p) {
  if (p == undefined) {
    return new MandatoryValueConstraintViolation(
        "No publication form provided!");
  } else if (!Number.isInteger( p) || p < 1 ||
      p > PublicationFormEL.MAX) {
    return new RangeConstraintViolation(
        `Invalid value for publication form: ${p}`);
  } else {
    return new NoConstraintViolation();
  }
}

The second check first tests if the argument is a non-empty array (representing a collection with at least one element) and then checks all elements of the array in a loop:

static checkPublicationForms( pubForms) {
  if (!pubForms || (Array.isArray( pubForms) &&
      pubForms.length === 0)) {
    return new MandatoryValueConstraintViolation(
        "No publication form provided!");
  } else if (!Array.isArray( pubForms)) {
    return new RangeConstraintViolation(
        "The value of publicationForms must be an array!");
  } else {
    for (const pF of pubForms) {
      const validationResult = Book.checkPublicationForm( pF);
      if (!(validationResult instanceof NoConstraintViolation)) {
        return validationResult;
      }
    }
    return new NoConstraintViolation();
  }
}

5.5. Write a serialization function

The object serialization function toString() now needs to include the values of enumeration attributes:

class Book {
  ...
  toString() {
    return `Book{ ISBN: ${this.isbn}, title: ${this.title},
    originalLanguage: ${this.originalLanguage},
    otherAvailableLanguages: ${this.otherAvailableLanguages.toString()},
    category: ${this.category},
    publicationForms: ${this.publicationForms.toString()} }`;
}

Notice that for the multi-valued enumeration attributes we call the toString() function that is predefined for JS arrays.

5.6. Data management operations

There are only two new issues in the data management operations compared to the validation app:

  1. We have to make sure that the cloneObject method, which is used in Book.update, takes care of copying array-valued attributes, which we didn't have before (in the validation app).

  2. In the Book.update method we now have to check if the values of array-valued attributes have changed, which requires to test if two arrays are equal or not. For code readability, we add an array equality test method to Array.prototype in browserShims.js, like so:

    Array.prototype.isEqualTo = function (a2) {
      return (this.length === a2.length) && this.every( (el,i) => el===a2[i]);
    }; 

    This allows us to express these tests in the following way:

    if (!book.publicationForms.isEqualTo( slots.publicationForms)) {
      book.publicationForms = slots.publicationForms;
      updatedProperties.push("publicationForms");
    }

5.7. Creating test data

In the test data records that are created by Book.createTestData(), we now have to provide values for single- and multi-valued enumeration attributes. For readability, we use enumeration literals instead of enumeration indexes:

Book.generateTestData = function () {
  try {
    Book.instances["006251587X"] = new Book({isbn:"006251587X", 
        title:"Weaving the Web", originalLanguage: LanguageEL.EN, 
        otherAvailableLanguages: [LanguageEL.DE, LanguageEL.FR], 
        category: BookCategoryEL.NOVEL, 
        publicationForms: [PublicationFormEL.EPUB, 
            PublicationFormEL.PDF]
    });
    ...
    Book.saveAll();
  } catch (e) {
    console.log(`${e.constructor.name}: ${e.message}`);
  }
};