The JS class model can be directly coded for getting the JS model classes of our app.
Code each class of the JS class model as a JS class with implicit getters and setters:
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.
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.
Write the code of the serialization functions
toString()
and toJSON()
.
Take care of deletion dependencies in the destroy
method.
These steps are discussed in more detail in the following sections.
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.
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; } ... }
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; } } } ... }
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
deleting all records of books published by p (Existential Dependency);
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.
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.