Model API
Constructor
Signature: new Model( uuid, options )
The constructor is creating an instance of any defined model at runtime to represent the related item. It is invoked with up to two arguments:
The uuid is uniquely addressing a record in connected datasource to be represented by this instance. It might be omitted on creating new item. In this case a UUID is assigned on saving this item for the first time.
The options object consists of several properties customizing the resulting item's behaviour:
- The property onUnsaved selects mode for handling multiple consecutive assignments to a single property without saving intermittently. See the description of
Model.onUnsaved
for additional information.
- The property onUnsaved selects mode for handling multiple consecutive assignments to a single property without saving intermittently. See the description of
Always start creation of items like this:
const item = new MyModel();
item.propertyA = someValue;
item.propertyB = anotherValue;
item.save().then( () => { ... } );
If you intend to gain access on an existing item you have to provide its UUID in constructor and load its record afterwards:
const item = new MyModel( "12345678-1234-1234-1234-1234567890ab" );
item.load().then( () => { ... } );
Static methods
Model.define()
Signature: Model.define( name, schema, baseClass, adapter ) : Model
This method is available to create a new model class according to provided definition. See the related documentation for defining models and on adapters for additional information.
Tightly Bound
A defined model is always bound to a certain backend using adapter provided here.
Model.list() 0.2.0+
Important
This method's signature has changed significantly starting with v0.2.0.
Signature: Model.list( queryOptions, resultOptions ) : Promise<Model[]>
This method promises an (excerpt from) unconditional list of current model's instances. It takes up to two sets of options. The first one is affecting the query, the second one is affecting the result.
Query-related options
- offset is an optional number of matching items to skip. The default is
0
. - limit is an optional number of matching items to return at most. The default is
Infinity
. - sortBy is the name of a property to sort resulting items by. When omitted resulting items aren't sorted at all.
- sortAscendingly is a boolean indicating whether resulting items should be sorted in ascending order or not. The default is true.
Performance
Sorting has a remarkable impact on performance. Defining index on any property you intend to sort items by is suggested to reduce this impact.
Result-related options
- loadRecords is a boolean requesting whether either listed item should have loaded all its properties on return already. This will have an impact on performance and thus you might like to focus on matching item's UUIDs. The default is true, thus you have to set this option false explicitly to prevent the penalty on performance.
- metaCollector may be an object which is receiving total number of matching items in property metaCollector.count. Fetching total number of matching items is affecting the performance for it needs to discover all existing items of model without regards to selected offset and limit query options. On the other hand implicitly fetching total count might save another query which is beneficial, as well.
Example
Model.list( {
offset: 10,
limit: 5,
}, {
loadRecord: false,
} )
.then( results => {
console.log( results.length ); // should display 5 at most
} )
FYI
This method is just an alias for using Model.find() with a particular query.
Model.find() 0.2.0+
Replacing findByAttribute()
Starting with v0.2.0 this method is replacing previously provided method Model.findByAttribute().
Signature: Model.find( query, queryOptions, resultOptions ) : Promise<Model[]>
This method is central to querying a collection of a model's items looking for model instances matching given query and related options.
Supported query options and result options have been described in context of Model.list() before. The query is an object describing a test to be performed on every item of the model to pick those to be retrieved. Its generic syntax is as follows:
At top level the query object consists of exactly one property with its name selecting a test to perform. The property's value is providing additional information customizing the test. It's syntax strongly depends on the selected test's type.
Model.find( { true: {} } );
This example is showing a query { true: {} }
. It is selecting a special type of test named true. This test is used internally to implement Model.list() for it is succeeding on every tested item of a model. The test doesn't require any additional information thus the property's value is just an empty object.
These types of tests are available currently:
Test Name | Type | Description |
---|---|---|
true | special | This test always succeeds. |
eq | comparison | Tests if value of a property is equal given value. |
neq | comparison | Tests if value of a property is not equal given value. |
in | comparison | Tests if value of a property is equal one out of multiple given values. |
lt | comparison | Tests if value of a property is less than a given value. |
lte | comparison | Tests if value of a property is less than or equal a given value. |
gt | comparison | Tests if value of a property is greater than a given value. |
gte | comparison | Tests if value of a property is greater than or equal a given value. |
between | comparison | Tests if value of a property is in range of a lower and an upper limit. |
null | unary test | Tests if named property is unset. |
notnull | unary test | Tests if named property is set. |
and | combining | Tests if all subordinated tests are matching. |
or | combining | Tests if one of the subordinated tests is matching. |
Comparison tests
All tests of type comparison are require provision of a property's name and a value to compare named property per item of model with.
Model.find( { eq: { name: "lastName", value: "Doe" } } )
This example is querying the model for all items with property lastName equal Doe.
Model.find( { lte: { name: "age", value: 50 } } )
This example is querying the model for all items with property age having value less than or equal 50.
A special case is the between
test for it requires provision of two parameters lower and upper instead of single parameter named value.
Model.find( { between: { name: "age", lower: 30, upper: 50 } } )
This example is querying the model for all items with property age having value between 30 and 50 inclusively.
In v0.7.4+ the in
test has been introduced as an extension to a single-value equality test. It takes a list of values instead of a single value and delivers all records with named property matching one of the listed values.
Model.find( { in: { name: "level", values: [ "entry", "medium" ] } } )
Using it is highly beneficial in looking up relations of a given set of records:
const companies = await Company.find( { eq: { name: "region", values: [ "us", "emea" ] } } );
const users = await User.find( { in: { name: "company", values: companies.map( c => c.uuid ) } } );
First, a list of companies in regions us
and emea
is fetched. After that, all users referring to either found company are fetched.
Unary tests
The test null
is provided to search items that don't have actual value for a given property. Using notnull
the opposite case can be tested. Either test doesn't require any additional parameter but the name of the property to check.
Model.find( { null: { name: "started" } } )
This example is querying the model for all items with unset property started.
Combining tests 0.7.3+
Multiple tests can be combined in a single query using tests and
and or
. They expect a list of tests as value:
Model.find( { and: [
{ gte: { age: 30 } },
{ lte: { age: 50 } }
] } )
This test delivers same results as the example given before on between
test. It matches all items with value of age
property being greater than or equal 30 and less than or equal 50.
Performance warning
Combining tests can significantly reduce performance. Most of all, this applies to queries lacking index.
- Use combining tests with care.
- Don't nest them too deeply.
- And make sure there is an index for every eventually tested property.
Don't use or
tests to look for records by a single property matching one of multiple values. Use in
comparison test, instead.
Reduced syntax 0.5.8+
For the comparison tests and unary tests listed above, there is a reduced syntax available. Instead of providing name
and value
in separate properties, a single property can be provided with its name equivalent to the property's name to test and its value equivalent to the value to test it with.
Binary operations like
Model.find( { lte: { name: "age", value: 50 } } )
can be reduced to
Model.find( { lte: { age: 50 } } )
A search for multiple values
Model.find( { in: { name: "level", values: [ "entry", "medium" ] } } )
can be reduced to
Model.find( { in: { level: [ "entry", "medium" ] } } )
The ternary operation between
can be reduced accordingly with both limits provided as a single array of values:
Model.find( { between: { age: [ 40, 50 ] } } )
For unary operations the reduced form is simply mapping the name of the operation into the name of the property this operation is applied to:
Model.find( { null: "age" } )
Model.fromObject() 0.4.3+
Signature: Model.fromObject( data, { omitComputed, serialized } ) : instance
Creates new instance of model instantly adopting values of properties in provided data object using resulting instance's method fromObject()
.
In addition provided object may include property uuid
to use it for identifying the resulting instance.
Model.uuidToKey()
Signature: Model.uuidToKey( uuid ) : string
When accessing a record of data stored in a connected datasource the instance's UUID is converted into a key suitable for selecting that record. This method implements the proper conversion.
Model.keyToUuid()
Signature: Model.keyToUuid( key ) : string
This method is the counterpart to Model.uuidToKey()
and may be used to convert keys provided by some backend into the UUID suitable for identifying a related instance of the model.
Model.getIndex() 0.2.0+
Signature: Model.getIndex( propertyName, indexType ) : Index
This method has been introduced to simplify access on a particular index. It is looking up Model.indices for the selected type of index covering given property. The result is undefined if there is no matching index or the instance managing the found index.
Model.uuidStream() 0.2.0+
Signature: Model.uuidStream() : Readable<Buffer>
The method returns a readable stream for the binary UUIDs of all items. The stream is an object stream with each provided object being a buffer consisting of 16 octets.
Model.normalizeUUID() 0.2.7+
Signature: Model.normalizeUUID( Buffer | string ) : Buffer
This method is provided for conveniently accessing code used internally to normalize and convert any provided UUID into its binary variant.
Model.formatUUID() 0.2.7+
Signature: Model.formatUUID( Buffer | string ) : string
This method is provided for conveniently accessing code used internally to normalize and convert any provided UUID into its string representation.
Static properties
The abstract Model
does not expose any static properties itself. But there static properties exposed by model classes compiled from a definition using Model.define()
. The following description refers to Model.*
to reflect this commonality between all compiled models that always derive from Model
.
Model.name
The name of model selected on defining it is exposed in context of model.
Model.adapter
This property exposes the adapter selected to persistently store instances of the model.
Model.schema
The qualified definition of model is exposed as its schema.
Model.indices
This array is a concise list of indices defined in context of current model. Every item in this list provides the property either index is used for and the type of index or comparison operation. Every item looks like this one.
{ property: "loginName", type: "eq" }
The exposed list is empty if there was no index defined for any of current model's properties.
Model.derivesFrom
This property refers to the class current model is derived from. The resulting class is always Model
or some class derived from Model
.
This reference can be used to invoked static methods of a model current one is deriving from:
static createRecord() {
this.derivesFrom.createRecord();
// add your code here
}
Model.onUnsaved 0.2.5+
By default, a model's instance prevents accidentally assigning twice to same property without saving intermittently. This also applies to loading an instance from datasource using instance.load()
after having assigned property values.
This behaviour is meant to prevent coding issues but might be an impediment in selected cases as well. Thus, it can be controlled per instance of a model using option onUnsaved
in second parameter of a model's constructor.
new MyModel( itemUuid, { onUnsaved: "ignore" } );
The default per model depends on this property which is used in case there is no option provided on constructing item. It might take up to three different values:
fail
is the default value and causes Error thrown when assigning twice to same property or on loading after assigning property.warn
prevents either action from throwing Error but cause log message on stderr.ignore
silently ignores those actions.
This property is defined as part of a schema's options section.
Model.notifications 0.6.0+
Notifications enable your server-side code to track changes to the data source. This is essential to establish highly reactive web applications and - beyond that - to improve Hitchy's support for horizontal scalability. Starting with version v0.6.0, the previous support for notifications has become a core feature of Odem and as such is now exposed per model via this property.
This property is an instance of EventEmitter emitting notifications as events. As such, every notification has a name and an optional list of additional arguments that are provided to registered listeners of either notification.
Supported notifications are:
created
When creating a new instance and save it to the data source, this notification is emitted with the created item's UUID and its serialized record as written to the data source. A final argument is a callback delivering a promise for an instance of current model representing that created record.
User.notifications.on( "created", ( uuid, newRecord, asyncGeneratorFn ) => {
// process the creation of a new user in the backend
} );
changed
This notification is emitted after saving adjusted properties of an existing instance of current model to the data source. It includes the item's UUID, the updated record in its serialized form as written to the data source and the previous record in its serialized form as found in the data source prior to replacing it. A final argument is a callback delivering a promise for an instance of current model representing that updated record.
User.notifications.on( "changed", ( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
// process the change of a new user in the backend
} );
removed
After removing a record from data source, this notification is emitted with the UUID of the removed item.
User.notifications.on( "removed", ( uuid ) => {
// process the removal of a user from the backend
} );
Instance methods
instance.load()
Signature: instance.load() : Promise<Model>
Promises current instances with values of its properties loaded from persistent datasource.
instance.save()
Signature: instance.save( optiosn ) : Promise<Model>
Promises properties of current instance persistently saved in datasource.
You may provide an object with options in first argument to customize behavior in selected use cases:
ignoreUnload
is a boolean option. When set, instance is saved to persistent storage without being loaded first.Usually, saving an instance without loading it first is ignored or rejected (if properties have been changed already) to prevent accidental damage to existing data. However, this is also preventing intentional creation of instances with particular UUIDs e.g. on restoring data from a backup.
instance.validate()
Signature: instance.validate() : Promise<string[]>
Validates values of every property in current instance promising a list of encountered validation errors. Validation is successful if promised list is empty, only.
instance.remove()
Signature: instance.remove() : Promise<Model>
Promises removal of current instance from datasource.
instance.toObject()
Signature: instance.toObject( { omitComputed, serialized } ) : object
Extracts values of all set properties of current instance as-is. By default, this includes values of computed properties, but excludes properties set null
currently.
Using options you might pass true
as option omitComputed
to omit computed properties. Separate option serialized
can be set to get all values serialized. This will prepare the resulting object for being serialized in turn, e.g. by using JSON.stringify()
.
instance.fromObject() 0.4.3+
Signature: instance.fromObject( data, { omitComputed, serialized } ) : instance
Adopts values of properties in provided data object for defined actual and computed properties of current model's instance. Any adopted value gets coerced.
Using options you might pass true
as option omitComputed
to ignore any property of provided data object matching a defined computed property of current instance. Separate option serialized
can be set to demand deserialisation of either adopted value before coercing it.
TIP
Due to current coercion implementations deserialisation isn't required in most cases but might be enabled to build future-proof implementations, e.g. when you are definitely processing data initially provided as JSON-formatted string.
WARNING
This method ignores any UUID included with provided data object. See the static counterpart for creating instances from such objects instead.
Instance properties
Basically, an instance of a model exposes every actual or computed property according to the model's definition. Those properties' names must not start with a $
by intention to prevent naming conflicts with any implicitly available property described here.
Note!
There is one exclusion from this rule of prefixing implicit properties with $
.
Every instance of a model is assumed to have a unique UUID for safely addressing it. This property is exposed as instance.uuid
. A model's definition mustn't use this name for any element in turn.
instance.$properties
The current instance's actual set of values per defined property is managed in a monitored object which is exposed as instance.$properties
.
For example, if you have defined a property name
for your model then there is a property instance.name
suitable for reading or writing related value of either instance of model. The actual value is managed as instance.$properties.name
internally.
It does not matter which way you access properties, but for the sake of simplicity and to create future-proof code you should use the exposed properties instead of instance.$properties
.
Internally, object-monitor is used to implement instance.$properties
. Thus it is possible to read several meta information regarding the managed set of property values, e.g.
detecting whether values have been changed recently by reading
instance.$properties.$context.hasChanged
,fetching map of recently changed values through
instance.$properties.$context.changed
, which is a Map mapping names of changed properties into either property's original value,committing or rolling back recent changes to properties using either
instance.$properties.$context.commit()
orinstance.$properties.$context.rollBack()
.Here, committing and rolling back does not refer to a transaction management usually found in a database management service, but to tracking or reverting local changes to properties of a model instance, only.
instance.$isNew
This property indicates whether current instance has been freshly created. This is assumed in case of missing UUID associated with current instance.
instance.$exists
This property exposes promise for indicator whether datasource contains representation of instance currently.
instance.$super
This property exposes object sharing prototype with the the class this model's class is derived from. Thus, it exposes that one's instance-related methods and properties.
The reference doesn't work like ES6 super
keyword but requires slightly more complex code when trying to invoke instance methods of parent class:
someMethod( arg1, arg2 ) {
this.$super.someMethod.call( this, arg1, arg2 );
// add your code here
}
instance.uuid
This property is different from other implicit properties for it doesn't start with $
by intention.
An instance's UUID is used to uniquely select it in its model's collection of instances. On freshly created instances the UUID is null
. After having saved such an instance the UUID assigned by backend adapter is exposed instead.
This property can be changed once after creating instances without UUID, only. The UUID might be given as string or as buffer of 16 octets.
instance.$uuid 0.2.0+
This property has been introduced in v0.2.0 to expose the binary UUID used internally. It isn't meant to change the UUID. Use instance.uuid
for that. See the related remarks there.
instance.$dataKey
This property is related to the instance's UUID and exposes the key used to address the related record in data source connected via backend adapter.
On a freshly created instance this information is a template containing %u
as a placeholder for the UUID to be assigned on saving.
instance.$api
When integrating with Hitchy its API is available via this property making it very easy to use components of your application such as services in hooks and methods of a model.
{
props: { ... },
hooks: {
beforeValidate() {
this.$api.runtime.services.ExtraValidator.check( this.$properties );
}
}
}
instance.$default 0.4.3+
Every property may be defined with a default value to be set on creating a model's instance. Whenever assigning value to either property it is possible to re-assign the declared default value by assigning this property, though it isn't either property's default value but some marker, only.
Example
Consider having model defined like this:
module.exports = {
name: "MyModel",
props: {
type: { default: "foo" },
score: { default: 50 },
},
};
This results in a model with properties named type
and score
. Creating new instance of this model will assign provided default implicitly:
const instance = new MyModel();
console.log( instance.type ); // "foo"
console.log( instance.score ); // 50
You can use this instance as usual and assign any value to its properties.
instance.type = "bar";
instance.score = 100;
console.log( instance.type ); // "bar"
console.log( instance.score ); // 100
Whenever you wish to return to the default you don't need to look it up in schema but assign instance.$default
instead:
instance.type = instance.$default;
instance.score = instance.$default;
console.log( instance.type ); // "foo"
console.log( instance.score ); // 50
instance.$notifications 0.6.0+
In addition to notifications emitted per model, another emitter for listening to instance-related notifications is exposed in this property. It is basically pulling notifications off the emitter of its model.
Supported notifications are:
changed
This notification is emitted after saving adjusted properties of an existing instance of current model to the data source. It includes the updated record in its serialized form as written to the data source and the previous record in its serialized form as found in the data source prior to replacing it.
user.$notifications.on( "changed", ( newRecord, oldRecord ) => {
// process the change
} );
While listening for the changed
notification in context of an instance like demonstrated, that instance's properties get instantly updated according to the reported change of record in backend unless there are local and unsaved changes to the instance.
removed
After removing a record from data source, this notification is emitted without any extra information. On receiving this notification, the instance's integration with notifications is shut down implicitly.
user.$notifications.on( "removed", () => {
// process the removal of `user` or the backend record it is representing
} );
Hooks
An item's life cycle consists of certain actions it is performing during its life as an instance of model's class at runtime. Some actions also affect its existence in a connected datasource.
Life cycle events
An item's life cycle includes these actions:
Action | Remarks |
---|---|
create | An instance of Model is constructed. |
load | An item's properties are loaded from connected datasource. |
validate | An item's properties are validated for being written to connected datasource afterwards. |
save | An item's properties are written to connected datasource. |
remove | An item's record is removed from connected datasource. |
For every action there is a pair of life cycle events: one occurs before either action and one occurs right after it (unless either action has failed). The events are named with prefixes before
or after
followed by related action's name resulting in camelCase name, e.g. beforeCreate
or afterValidate
.
For every supported life cycle event there is an instance method of same name to be invoked whenever either event occurs. When defining a model those hooks can be replaced in section hooks.
Asynchronous Hooks
By intention all hooks but beforeCreate
and afterCreate
may return a promise to delay further processing. Whenever one of those hooks is required to return data that data may be promised as well.
beforeCreate
and afterCreate
can't delay processing for being called in model's constructor which has to work synchronously.
Calling Parent Class Hooks
In combination with instance.$super
any code is capable of including related hook of parent class.
afterValidate( errors ) {
const errors = this.$super.afterValidate.call( this, errors );
// add your code here
return errors;
}
In addition, though, you must be aware of either hook of parent class might return promise as well requiring your hook to wait for the promise to be settled and adopt its result either way.
afterValidate( errors ) {
return Promise.resolve( this.$super.afterValidate.call( this, errors ) )
.then( errors => {
// add your code here
return errors;
} );
}
instance.beforeCreate()
Signature: instance.beforeCreate( { uuid, options } ) : { uuid, options }
When creating an item by means of wrapping it in an instance of defined model's class this hook is invoked. Given constructor arguments it is required to return the constructor arguments as provided or adjust them to be used instead.
The uuid is given as instance of Buffer or as string. The options object might contain a reference on a backend adapter to use instead of model's default adapter. In addition there might be a mode for handling multiple consecutive assignments to a property without saving intermittently.
Important!
Talking about creating an item refers to creating an instance in runtime. It doesn't mean that there is a new item currently missing in connected datasource. It is just about the moment when a runtime instance of this model is constructed to represented the item.
No Promise!
For being invoked in context of new instance's constructor this hook can't delay processing by returning a promise. Any returned promise is ignored.
instance.afterCreate()
Signature: instance.afterCreate()
This hook is invoked at the very end of a new instance's constructor.
No Promise!
For being invoked in context of new instance's constructor this hook can't delay processing by returning a promise. Any returned promise is ignored.
instance.beforeLoad() 0.2.7+
Signature: instance.beforeLoad()
This hook is invoked right before loading item's record from connected datasource.
instance.afterLoad() 0.2.7+
Signature: instance.afterLoad( object ) : object
Whenever reading an instance's raw record from connected datasource, this optional hook is invoked for optionally adjusting that raw record provided in first argument prior to further processing. The hook's return value is used instead of loaded raw record to populate properties of instance, eventually.
instance.beforeValidate()
Signature: instance.beforeValidate() : Error[]
This hook is invoked before validating all properties of current instance to comply with defined constraints. It might return a list of errors to be extended with errors encountered by definition-based validation performed afterwards.
instance.afterValidate()
Signature: instance.afterValidate( Error[] ) : Error[]
This hook is invoked after validating all property values of current instance to comply with defined constraints. The list of error messages is provided in first argument as an array of Error
instances. The probably adjusted list of messages is meant to be returned by this hook.
Important
Any validation of properties fails if there is at least one error in the list. By default, this results in rejecting whole set of instance's properties being sent to connected datasource for saving.
When implementing this hook, you should always return the provided list of errors at least to prevent invalid property values from being stored in connected datasource.
instance.beforeSave()
Signature: instance.beforeSave( boolean, object, boolean ) : object
This hook is invoked prior to persistently saving property values of current instance in a datasource connected via backend adapter.
First argument indicates whether there is an existing record in connected datasource (true
) or not.
Second argument is the serialized ("raw") record of instance's properties to be saved in connected datasource. The hook is required to return that record as-is or apply some modifications before returning it eventually.
Third argument has been added in 0.7.5+. It is indicating if a fresh UUID will be assigned to the instance by connected datasource on actually storing it there. In this hook, the instance does not have a UUID yet if this argument is set.
instance.afterSave()
Signature: instance.afterSave( boolean, boolean )
This hook is invoked after saving property values of current instance in a datasource connected via backend adapter. First argument indicates whether there was an existing record in connected datasource (true
) before saving this time or not.
Second argument has been added in 0.7.5+. It is indicating if a fresh UUID has been assigned to the instance by connected datasource on actually storing its properties there. Because of that, the instance always has a UUID in context of this hook.
instance.beforeRemove()
Signature: instance.beforeRemove()
This hook is invoked prior to removing current instance. It may throw an exception or reject some returned promise to prevent the removal of current instance.
instance.afterRemove()
Signature: instance.afterRemove()
This hook is invoked after having removed current instance. The hook is invoked in context of removed instance.