typed-model
JavaScript library for defining types and their properties with support for wrapping/unwrapping and serialization/deserialization.
Installation
npm install typed-model --save
Overview
The typed-model
module provides utility code for defining data model types.
These data model types provide helpful accessor methods (getters and setters)
for the properties defined for the model type.
These models can be thought of as a "schema" that provide extra safeguards for working with objects. These model types are not tied to a specific data storage backend so you can use these in the browser or on the server-side with very little overhead. This approach to defining your schema is similar to Mongoose schemas except that this library is not tied to MongoDB or any other storage engine.
If you application is fetching data from the database for a client request and
there is no need to process the data, then simply serialize the data
without creating Model
instances to wrap the data. Creating Model
instances
creates unnecessary overhead with no benefit (this is the default behavior
of Mongoose). However, if you're
accessing or setting properties on an object then you might find it
helpful to wrap the raw object with a Model
instance and use the
getters and setters to work with the data.
Relationship to JSON Schema
Model definitions are similar to JSON Schema and, when possible, similar naming conventions were chosen. However, this module is more tailored to runtime usage. If desired, you can convert your model definitions to a JSON Schema representation fairly easily. See JSON Schema section for more information.
Usage
Requiring
// Requiring the base Model typevar Model = ; // Requiring the Enum type factoryvar Enum = ; // Convenience function to create new Modelvar NewModel = ; // Convenience function to create new Enum typevar NewEnum = ;
Primitive Types
The following primitive types are supported:
- Date Type:
Date
/"date"
/require("typed-model/Date")
- Boolean Type:
Boolean
/"boolean"
/require("typed-model/Boolean")
- Number Type:
Number
/"number"
/require("typed-model/Number")
- Integer Type:
"integer"
/require("typed-model/Integer")
- String Type:
String
/"string"
/require("typed-model/String")
- Array Type:
Array
/[]
/"array"
/require("typed-model/Array")
Complex Object Type
Declare custom complex object type:
var Address = Model;
Create instance via new
constructor with no initial data:
// Create via constructor with no initial datavar address = ;address;address;
Create instance via new
constructor with some initial data:
// Create via constructor with initial datavar address = city: 'San Francisco' state: 'CA';
Create instance via create
method:
// Create via "create" functionvar address = Address;address;address;
Create instance by wrapping existing data:
// Create via "wrap" functionaddress = Address;
Types that Implement EventEmitter
Types that extend Model
will not implement the EventEmitter
interface.
If your type should be an EventEmitter
then your type should either extend
require('typed-model/ObservableModel')
or add the EventEmitter
mixin.
Types that implement EventEmitter will emit change
and change:someProperty
events
Example using ObservableModel:
var Something = ;
Example using mixin:
var Something = ;
Listening for property value changes:
var something = ; something; something;
Self-type References in Properties
In some use cases, the type of a property is the same type as the complex object for which the property is declared. For exampled, to build a linked list, each node has a pointer to the next node.
Here are some examples of self-type references:
// Declare a linked list node type that has a pointer// to the next nodevar LinkedListNode = Model; // Here is another functionally equivalent variation of LinkedListNodevar LinkedListNode = Model; // An example of self-type reference within an arrayvar TreeNode = Model; // Here is another functionally equivalent variation of TreeNodevar TreeNode = Model;
Getters and Setters
A getter and setter will be generated on the prototype, for each property defined in the model.
For example:
// Define an Address modelvar Address = Model; // Create instance of Addressvar address = ; // Use the generated setter to set the cityaddress; // Use the generated getter to get the city
Note: The getter function name will be always in the form
get<PropertyName>
. The setter function name will be always in the form
set<PropertyName>
. These rules do not change for properties with Boolean type.
Model prototype
The Model
types are created via standard prototypical inheritance.
If you wish to conveniently add other methods or properties to the
prototype then use use the prototype
property in the Model
configuration.
For example:
var Person = Entity;
Inheritance
Define your base Entity type:
var Entity = Model;
Define a type that extends Entity:
var Person = Entity;
The new Person
type will recognize email
(defined for Person
) and
id
(defined for Entity
) as properties.
var person = ;person;person;
You can also create getters for computed/derived properties.
For example:
var Person = Entity;
Non-persisted Properties
If you'd like to store computed properties in the Model instance for performance reasons but you don't want them to be persisted to storage, then you might want to mark an property as non-persisted.
For example, here's a Model type that will automatically
update displayName
whenever firstName
or lastName
is changed:
{ person;} var Person = Entity; var person = firstName: 'John' lastName: 'Doe'; ; // Remove non-persisted propertiesvar personObj = person;;
Wrap/Unwrap
Model.unwrap(obj)
can be safely called with any object. If the
given obj
is a model then it will be unwrapped via obj.unwrap()
.
If the given obj
is not a model then the obj
will simply be
returned.
SomeType.wrap(obj)
can be used to ensure that the given obj is
wrapped as SomeType
. If obj
is already SomeType
then obj
will simply be returned.
Note, an unwrapped object that has been previously wrapped will
have a $model
property inside of it that stores a cached value
of the actual model instance. This allows for very efficient
wrapping and unwrapping without creating a lot of new objects
in the heap. If you want to ensure that your model is not
"polluted" with this metadata then use obj.clean()
or
Model.clean(obj)
which will create a cloned version of obj
without any extra metadata or non-persisted properties.
Examples:
var Address = Model; var address = city: 'San Francisco' state: 'CA'; // Create an instance of Addressvar addressObj = Model;; // Wrap the unwrapped objectvar addressWrapped = Address;; // The wrapped object returned by Address.wrap()// will be the original Model instance that we created.;
Clean
Model.clean(obj)
should be used to return a clone of an object in which
all non-persisted properties and metadata have been removed. The clean
function will always return a deep clone of the given object if the
given argument is non-null and not a primitive.
var address = city: 'San Francisco' state: 'CA'; // When saving a model object to disk or storage, use clean to remove// unnecessary fields.db;
A Model
can also control how its data is cleaned by providing a clean
property. For example, this might be helpful for working with binary data
by automatically encoding the binary data as a base64 string.
A Model
type that is not wrapped (that is, when wrap: false
flag is provided),
its value will not be cleaned unless a function
is provided for the clean
property.
Here's an example how to use the clean
function to convert a Buffer
to a Base64 encoded string:
var Binary = Model; var Image = Model; var image = // data can be provided as Array of bytes, base64 encoded string, or Buffer // because Binary.coerce function handles each of these. data: someData; // the data will be converted to Buffer object via Binary.coerce function; // Calling clean on the image model instance will cause the contained data// to be converted to base64 string via Binary.clean function.var cleanedImage = image; // the data will be converted to String via Binary.clean function; // log the Base64 encoded stringconsole;
Stringify
Model instances have a stringify
function that can be used to
safely stringify the instance.
For example:
// Stringify and do not add extra white-spaceconsole; // Stringify and include extra white-space for better readabilityconsole;
Type Coercion
As a developer, you may choose to be lenient about how certain non-Model instances are coerced into instances of a Model.
For example, consider this example of declaring ObjectId
type that
automatically coerces Strings to actual instances of require('mongodb').ObjectID
:
var MongoDbObjectID = ObjectID; var ObjectId = Model; var Entity = Model
Models that use the primitive Date
type also benefit from type coercion.
The Date
coerce function provided by typed-model
automatically convert
strings in ISO date format to Date
instances. You will probably find this helpful because,
by default, JSON.stringify(obj)
will automatically convert Date
objects
to Strings using the standard ISO format.
For example:
var Document = Model; var document = ;// A String value in ISO date format is automatically converted to a// real Date.document;
Enum Type
String enum values::
var Color = Enum; // The following assertions will be true;;;;
Object enum values:
var Color = Enum; // The following assertions will be true:;;;;;;
Loop over values:
Colorvalues;
Loop over names:
Colornames;
Array Type
Syntax:
var Color = Enum; var ColorPalette = Model;
Short-hand syntax:
var ColorPalette = Model;
Accessing an array property:
var colorPalette = colors: 'red' 'green' 'blue'; // getColors() will return an Array and we can use the "forEach" function.// Each item in the returned Array will be an instance of Color.colorPalette;
Object Validation
Using array to capture errors:
// array that will collect errorsvar errors = ; // collect errors while wrapping existing person datavar person1 = Person; // collect errors while constructing new personvar person2 = name: 'John' age: 'bad integer' errors;
Using extended options:
var options = // array that will collect errors errors: // strict mode is used by some primitive types to require // that values be of the same primitive types // (no automatic type coercion) strict: true; var person = name: 'John' age: 'bad integer' options;
JSON Schema
A Model
type can be easily converted to an equivalent JSON schema
with the following module:
var jsonSchema = ;var someModelSchema = jsonSchema;
Option | Type | Purpose |
---|---|---|
toRef |
function(Model) |
This function can be used to turn a Model definition to a reference name (return value will be used as value for $ref properties) |
isIgnoredProperty |
function(name, property) |
This function can be used to exclude a property from the schema definition of a complex object |
Convert Model to JSON Schema
Define your models:
var Model = ;var Enum = ; var Entity = Model; var Gender = Enum; var Species = Enum; var Pet = Model; var Person = Entity;
Convert your model to JSON schema:
var jsonSchema = ;var jsonSchemaOptions = { return ModeltypeName; }; var EntitySchema = jsonSchema;var GenderSchema = jsonSchema;var SpeciesSchema = jsonSchema;var PetSchema = jsonSchema;var PersonSchema = jsonSchema;
Entity JSON Schema:
Gender JSON Schema:
Species JSON Schema:
Pet JSON Schema:
Person JSON Schema: