Missy
Missy is a slim database-agnostic data mapper for NodeJS with pluggable drivers.
Whenever you need a truly flexible, lightweight DB tool - she's here for you.
Quick overview:
- Database-agnostic. Currently, PostgreSQL and MongoDB are supported
- Allows to customize the DB client object
- Full CRUD operations support
- Custom update/remove operations
- Easy custom data types
- Absolutely no limitations on the underlying schema and keys
- Model events & hooks for full control
- Rich data selection control: projections, limit/offset, sorting
- Model relations, even across databases
- Supports schema-less NoSQL documents with custom fields
- MongoDB-style API
- Reliable DB connection handling
- Promise-based: uses the q package
- Amazingly simple and well-structured
- Documented and rich on comments
- 100% tests coverage
Table Of Contents
- Glossary
- Tutorial
- Core Classes
- Driver
- Schema
- Model
- Recipes
Glossary
Commonly used terms:
- Driver
- Database driver that handles low-level Missy queries like find, insert, save, etc.
- Schema
- A collection of Models bound to a database Driver. Also, Type Handlers are defined on it.
- Type Handler
- A class that converts Field values to/from the database
- Entity
- An entity is a document persisted in the database
- Model
- A Model defines fields on some DB namespace (table, collection, etc) and provides methods to access Entities.
- Field
- Fields are defined on a model and specify its name and type. Other options are available
- Relation
- A relation is a way of accessing the associated entities
Tutorial
Let's peek at how Missy works:
var missy = MongodbDriver = ; // Drivervar mongoDriver = { // Connecter function } // driver options journal: true; // Schemavar schema = mongoDriver queryWhenConnected: false // when disconnected, queries will wait for the driver to connect; // Modelsvar User = schema; var Post = schema; var Comment = schema; // RelationsUser; // name, model, fields mappingPost;Comment;Post; // Model hooksUserhooks{ // adds a synchronous hook // validate sex if 'm''f' === -1 entitysex = '?';}; Userhooks; // Connect & action!schema // Saving entities // Fetching entities // Removing entities // Updating an entity without loading it (!) // Removing an entity without loading it (!) ;
Core Classes
Most Missy methods use these so you'll need to know how to work with them. This includes:
- Converting values to/from the database format
- Specifying field projections
- Search criteria format
- Sorting entities
- Specifying the update expressions
Though most of them are designed after MongoDB, they're applicable to all DB drivers with no limitations.
Converter
Source: lib/util/model.js#Converter
Converter
transparently transparently converts field values to/from the DB format.
NOTE: Converter
does not touch fields that are not defined in the model! Keep this in mind when working with
NoSQL databases as in general documents can contain arbitrary fields.
Consider the example:
var User = schema
On save, the 'id' field is converted to a number, name and login - to string, tags - to array. Also, 'ctime' is set to the current time as no value was not provided.
On find, the query criteria 'id' field is converted to a number, and the resulting entity is converted back.
In general, Converter
does the following:
- Sets default values on fields that are
undefined
and havedef
field property set to either a value or a function. - Applies type handlers to entity fields defined in the model.
Type Handlers
Converter
is driven by type handlers: classes which can convert a JS value to the DB format and back.
Each type handler has a name to be referenced in the model fields definition. Alternatively, you can use a JS built-in object as a shortcut.
Missy defines some built-in type handlers in lib/types/:
Name | Shortcut | JS Type | Default | Comment |
---|---|---|---|---|
any | undefined | * | undefined | No-op converter to use the value as is |
string | String | String,null |
'', null |
Ensure a string, or null |
number | Number | Number,null |
0, null |
Ensure a number, or null |
boolean | Boolean | Boolean,null |
false, null |
Ensure a boolean, or null |
date | Date | Date,null |
null |
Convert to JS Date , or null |
object | Object | Object,null |
{}, null |
Use a JS Object , or null . |
array | Array | Array,null |
[], null |
Ensure an array, or null . Creates arrays from scalar values |
json | - | String,null |
null |
Un/serializes JSON, or null . Throws MissyTypeError on parse error. |
Note: most built-in types allow null
value only when the field is not defined as required
(see below).
Note: DB drivers may define own type handlers and even redefine standard types for all models handled by the driver.
Also, the following aliases are defined:
Alias | Definition |
---|---|
bool | { type: 'boolean' } |
int | { type: 'number' } |
float | { type: 'number' } |
Custom Type Handlers
A type handler is a class that implements the IMissyTypeHandler
interface (lib/interfaces.js):
- Constructor receives 2 arguments: the schema to bind to, and the type handler name.
- Method
load(value, field)
which converts a value loaded from the database - Method
save(value, field)
which converts a value to be stored to the database - Method
norm(value, field)
which normalizes the value - Has 2 properties:
schema
andname
Once the type handler is defined, you register in on a Schema
:
var stringTypeHandler = typesString;schema; // Now you can use it on a model:var User = schema;
Note: built-in types are registered automatically, there's no need to follow this example.
MissyProjection
Source: lib/util/model.js#MissyProjection
When selecting entities from the DB, you may want to fetch a subset of fields. This is what MissyProjection
does:
allows you to specify the fields to include or exclude from the resulting entities.
MissyProjection
closely follows
MongoDB Projections specification.
A projection can be used in one of the 3 available modes:
- all mode. All available fields are fetched.
- inclusion mode. Fetch the named fields only.
- exclusion mode. Fetch all fields except the named ones.
The following projection input formats are supported:
-
String syntax. Projection mode + Comma-separated field names.
'*'
: include all fields'+a,b,c'
: include only fields a, b, c'-a,b,c'
: exclude fields a, b, c
-
Array syntax. Array of field names to include.
[]
: include all fields['a','b','c']
: include only fields a, b, c- Exclusion mode is not supported
-
Object syntax. MongoDB-style projection object.
{}
: include all fields{ a:1, b:1, c:1 }
: include only fields a, b, c{ a:0, b:0, c:0 }
: exclude fields a, b, c
-
MissyCriteria. A
MissyCriteria
object.
Usage example:
var User = schema; User // only fetch 2 fields: id, login ;
MissyCriteria
Source: lib/util/model.js#MissyCriteria
Specifies the search conditions.
Implements a simplified version of
MongoDB collection.find
criteria document:
- Use
{ field: value, .. }
syntax to match entities by equality ; - Use
{field: { $operator: value, .. }, .. }
syntax to compare with an operator.
Example:
id: 1 // equality login: $eq: 'kolypto' // equality with an operator role: $in: 'admin''user' // $in operator example age: $gt: 18 $lt: 22 // 2 operators on a single field
Note: To keep the implementation simple and effective, Missy does not support complex queries and logical operators. If you need them, see Using The Driver Directly.
The following operators are supported:
Operator | Definition | Example | Comment |
---|---|---|---|
$gt |
> | { money: { $gt: 20000 } } |
Greater than |
$gte |
<= | { height: { $gte: 180 } } |
Greater than or equal |
$eq |
== | { login: { $eq: 'kolypto' } } |
Equal to |
$ne |
!= | { name: { $ne: 'Lucy' } } |
Not equal to |
$lt |
< | { age: { $lt: 18 } } |
Lower than |
$lte |
<= | { weight: { $lte: 50 } } |
Lower than or equal |
$in |
IN | { role: { $in: ['adm', 'usr' ] } } |
In array of values. Scalar operand is converted to array. |
$nin |
NOT IN | { state: { $nin: ['init','error'] } } |
Not in array of values. Scalar operand is converted to array. |
Before querying, MissyCriteria
uses Converter
to convert the given field values to the DB types.
For instance, the { id: '1' }
criteria will be converted to { id: { $eq: 1 } }
.
Example:
var User = schema; User // 18 <= age <= 22 ;
MissySort
Source: lib/util/model.js#MissySort
Defines the sort order of the result set.
MissySort
closely follows the
MongoDB sort specification.
The following sort input formats are supported:
-
String syntax. Comma-separated list of fields suffixed by the sorting operator:
+
or-
.a,b+.c-
: sort by a asc, b asc, c desc
-
Array syntax. Same as String syntax, but split into an array.
['a', 'b+', 'c-' ]
: sort by a asc, b asc, c desc
-
Object syntax. MongoDB-style object which maps field names to sorting operator:
1
or-1
.{ a: 1, b: 1, c: -1 }
: sort by a asc, b asc, c desc
Example:
var User = schema; User // sort by `age` descending ;
MissyUpdate
Source: lib/util/model.js#MissyUpdate
Declares the update operations to perform on matching entities.
MissyUpdate
implements the simplified form of
MongoDB update document.
- Use
{ field: value, .. }
syntax to set a field's value ; - Use
{ $operator: { field: value } }
syntax to apply a more complex action defined by the operator.
Example:
mtime: // assign a value $set: mtime: // assign a value $inc: hits: +1 // increment a field $unset: error: '' // unset a field $setOnInsert: ctime: // set on insert, not update $rename: 'current': 'previous' // rename a field
The following operators are supported:
Operator | Comment |
---|---|
$set |
Set the value of a field |
$inc |
Increment the value of a field by the specified amount. To decrement, use negative amounts |
$unset |
Remove the field (with some drivers: set it to null ) |
$setOnInsert |
Set the value of a field only when a new entity is inserted (see upsert with Model.updateQuery) |
$rename |
Rename a field |
Before querying, MissyUpdate
uses Converter
to convert the given field values to the DB types.
For instance, the { id: '1' }
criteria will be converted to { $set: { id: 1 } }
.
Example:
var User = schema;User // update mtime without fetching ;
Driver
Missy is a database abstraction and has no DB-specific logic, while Drivers implement the missing part.
A Driver is a class that implements the IMissyDriver
interface (lib/interfaces.js).
Each driver is created with a connecter function: a function that returns a database client through a promise. Missy does not handle it internally so you can specify all the necessary options and tune the client to your taste.
The first thing you start with is instantiating a driver.
var missy = ; // For demo, we'll use `MemoryDriver`: it does not require any connecter function at all.var memory = ;
Note: MemoryDriver
is built into Missy, but is extremely slow: it's designed for unit-tests and not for production!
At the user level, you don't use the driver directly. However, it has two handy events:
memory; memory;
Each driver has (at least) the following properties:
client:*
: The DB clientconnected:Boolean
: Whether the client is currently connected
Supported Drivers
Missy drivers are pluggable: just require another package, and you'll get a new entry under missy.drivers
.
Driver | Database | Package name | Github |
---|---|---|---|
MemoryDriver |
in-memory | missy | built-in |
PostgresDriver |
PostgreSQL | missy-postgres | https://github.com/kolypto/nodejs-missy-postgres |
MongodbDriver |
MongoDB | missy-mongodb | https://github.com/kolypto/nodejs-missy-mongodb |
Contributions are welcome, provided your driver is covered with unit-tests :)
Schema
Source: lib/Schema.js#Schema
The instantiated driver is useless on its own: you just pass it to the Schema
object which is the bridge that connects
your Models with the Driver of your choice.
You're free to use multiple schemas with different drivers: Missy is smart enough to handle them all, with no limitations.
Note: a single driver can only be used with a single schema!
var missy = ; // Create: Driver, Schemavar driver = schema = driver {}; // Initially, the schema is not connected schema ;
Schema(driver, settings?)
Constructor. Creates a schema bound to a driver.
-
driver:IMissyDriver
: The driver to work with -
settings:Object?
: Schema settings. An object:-
queryWhenConnected: Boolean
Determines how to handle queries on models of a disconnected schema.
When
false
(default), querying on a disconnected schema throwsMissyDriverError
When
true
, the query is delayed until the driver connects.
Source: lib/options.js#SchemaSettings
-
Initially, the schema is not connected. Use Schema.connect()
to make the driver connect.
A Schema
instance has the following properties:
driver:IMissyDriver
: The driver the schema is bound tosettings:Object
: Schema settings objecttypes:Object.<String, IMissyTypeHandler>
: Custom type handlers defined on the schemamodels:Object.<String, Model>
: Models defined on the schema
Schema.define(name, fields, options?):Model
Defines a model on the schema. The model uses the driver bound to the schema.
Note: you can freely define models on a schema that is not connected.
name:String
: Model namefields:Object
: Model fields definitionoptions:Object?
: Model options
See: Model Definition.
schema;
Schema.registerType(name, TypeHandler):Schema
Register a custom Type Handler on this schema. This type becomes available to all models defined on the schema.
name: String
: The type handler name. Use it in model fields:{ type: 'name' }
.TypeHandler:IMissyTypeHandler
: The type handler class to use. Must implementIMissyTypeHandler
See: Custom Type Handlers
Schema.connect():promise
Ask the driver to connect to the database.
Returns a promise.
schema ;
Note: once connected, the Schema automatically reconnects when the connection is lost.
Schema.disconnect():promise
Ask the driver to disconnect from the database.
Returns: a promise
schema ;
Note: this disables automatic reconnects until you explicitly connect()
.
Schema.getClient():*
Convenience method to get the vanilla DB client of the underlying driver.
Handy when you need to make some complex query which is not supported by Missy.
See: Using The Driver Directly
Model
Source: lib/Model.js#Model
A Model is the representation of some database namespace: a table, a collection, whatever. It defines the rules for a certain type of entity, including its fields and business-logic.
Once defined, a model has the following properties:
schema:Schema
: The Schema the Model is defined onname:String
: Model namefields:Object.<String, IModelFieldDefinition>
: Field definitionsoptions:Object
: Model optionsrelations:Object.<String, IMissyRelation>
: Relation handlers
Model Definition
To define a Model
on a Schema
, you use schema.define()
:
var missy = ; var driver = schema = driver {}; var User = schema;
Schema.define
accepts the following arguments:
name:String
: Model name.fields:Object
: Model fields definitionoptions:Object?
: Model options
Fields Definition
Source: lib/interfaces.js#IModelFieldDefinition
As stated in the type handlers section, a field definition can be:
-
A native JavaScript type (
String
,Number
,Date
,Object
,Array
) -
A Field type handler string name:
'string'
,'any'
, etc. -
An object with the following fields:
-
type:String
: Field type handler -
required:Boolean?
: Is the field required?A required field is handled differently by type handlers: it is now allowed to contain
null
and gets some default value determined by the type handler.Default value: uses
required
from model options. -
def:*|function(this:Model)
: The default value to use when the field isundefined
.Can be either a value, or a function that returns a value.
The default value is used when an entity is being saved to the DB, and the field value is either
undefined
, ornull
and having therequired=true
attribute.
-
Model Options
Source: lib/options.js#ModelOptions
-
table:String?
: the database table name to use.Default value: Missy casts the model name to lowercase and adds 's'. For instance, 'User' model will get the default table name of 'users'.
-
pk:String|Array.<String>?
: the primary key field name, or an array of fields for compound PK.Every model in Missy needs to have a primary key.
Default value:
'id'
-
required:Boolean?
: The default value for fields'required
attribute.Default value:
false
-
entityPrototype: Object?
: The prototype of all entities fetched from the database.This is the Missy way of adding custom methods to individual entities:
var Wallet = schema;Wallet // assuming there exists { uid: 1, amount: 100, currency: 'USD' };
Helpers
The following low-level model methods are available in case you need to manually handle entities that were fetched or are going to be stored by using the driver directly:
Model.getClient():Object
Returns the vanilla DB client from the Schema.
Model.entityImport(entity):Q
Process an entity (or an array of them) loaded from the DB, just like Missy does it:
- Invoke model hooks (see: Model Hooks)
- Use
Converter
to apply field types - Assign entity prototypes (if configured)
Returns a promise for an entity (or an array of them).
var Post = schema; Post ;
Model.entityExport(entity):Q
Process an entity (or an array of them) before saving it to the DB, just like Missy does it:
- Invoke model hooks (see: Model Hooks)
- Use
Converter
to apply field types
Returns a promise for an entity (or an array of them).
Operations
All examples use the following schema:
var User = schema;
All promise-returning methods can return the following error:
MissyDriverError
: driver-specific error
Read Operations
Model.get(pk, fields?):Q
Get a single entity by its primary key.
Arguments:
-
pk: *|Array|Object
: The Primary Key value. For compound primary keys, use array or object of values.Accepts the following values:
pk: *
: Any scalar primary key value. Only applicable for single-column Primary Keys.pk: Array
: An array of PK values. Use with compound Primary Keys.pk: Object
: PK values as an object.
-
fields: String|Array|Object|MissyProjection?
: Fields projection.
Returns: a promise for an entity, or null
when the entity is not found.
Errors:
MissyModelError
: invalid primary key: empty or incomplete.
// single-column PKvar User = schema; User // get User(id=1) ; User // User(id=1) ; only fetch fields `id`, `login` ; User // User(id=1) ; only fetch fields `id`, `login` ; // multi-column PKvar Post = schema; User // get Post(uid=1,id=15) ; User // get Post(uid=1,id=15) ; omit field `roles` ; User // incomplete PK ;
Model.findOne(criteria?, fields?, sort?, options?):Q
Find a single entity matching the specified criteria. When multiple entities are found - only the first one is returned.
Arguments:
criteria: Object|MissyCriteria?
: Search criteriafields: String|Object|MissyProjection?
: Fields projectionsort: String|Object|Array|MissySort?
: Sort specificationoptions: Object?
: Driver-specific options, if supported.
Returns: a promise for an entity, or null
when no entity is found.
User;
Model.find(criteria?, fields?, sort?, options?):Q
Find all entities matching the specified criteria.
Arguments:
-
criteria: Object|MissyCriteria?
: Search criteria -
fields: String|Object|MissyProjection?
: Fields projection -
sort: String|Object|Array|MissySort?
: Sort specification -
options: Object?
: Driver-specific options, if supported.Options supported by all drivers:
skip: Number?
: The number of entities to skip. Default:0
, no skiplimit: Number?
: Limit the returned number of entities. Default:0
, no limit
Returns: a promise for an array or entities.
User;
Model.count(criteria?, options?):Q
Count entities that match the criteria
Arguments:
criteria: Object|MissyCriteria?
: Search criteriaoptions: Object?
: Driver-specific options, if supported.
Returns: a promise for a number.
User ;});
Write Operations
Model.insert(entities, options?):Q
Insert a new entity (or an array of them).
Arguments:
entities: Object|Array.<Object>
: a single entity, or an array of them.options: Object?
: Driver-specific options, if supported.
Returns: a promise for the inserted entity (or an array of them).
Errors:
EntityExists
: the entity already exists
Notes:
- The drivers are required to return entities as saved by the DB, and in the order matching the input.
- If any entity already exists, the driver throws an exception and stops. Entities that were already inserted are not removed.
// Insert a single entityUser ; // Insert an array of entitiesUser;
Model.update(entities, options?):Q
Update (replace) an existing entity (or an array of them).
Arguments:
entities: Object|Array.<Object>
: a single entity, or an array of them.options: Object?
: Driver-specific options, if supported.
Returns: a promise for the updated entity (or an array of them).
Errors:
EntityNotFound
: the entity does not exist
// Insert a single entityUser ;
Model.save(entities, options?):Q
Save an arbitrary entity (or an array of them). This inserts the missing entities and updates the existing ones.
Arguments:
entities: Object|Array.<Object>
: a single entity, or an array of them.options: Object?
: Driver-specific options, if supported.
Returns: a promise for the entity (or an array of them).
User
Model.remove(entities, options?):Q
Remove an existing entity (or an array of them) from the database.
Arguments:
entities: Object|Array.<Object>
: a single entity, or an array of them.options: Object?
: Driver-specific options, if supported.
Returns: a promise for the entity (or an array of them).
Errors:
EntityNotFound
: the entity does not exist
User // PK is enough ;
Queries
Model.updateQuery(criteria, update, options?):Q
Update entities that match a criteria using update operators. This tool allows to update entities without actually fetching them. DB drivers do this atomically, if possible.
Arguments:
-
criteria: Object|MissyCriteria
: Search criteria -
update: Object|MissyUpdate
: Update operations -
options: Object?
: Driver-specific options, if supported.Options supported by all drivers:
upsert: Boolean?
: Do an upsert: when no entity matches the criteria, a new entity is built from the criteria (equality operators converted to values) and update operators applied to it. Default:false
multi: Boolean?
: Allow updating multiple entities. Inmulti=true
mode, the method returns an array. Default:false
Returns: a promise for a full updated entity or null
when no matching entity is found.
When multi=true
, returns an array of entities.
// update a single entityUser
Model.removeQuery(criteria, options?):Q
Remove entities that match a criteria. This tool allows to remove entities without actually fetching them. DB drivers do this atomically, if possible.
Arguments:
-
criteria: Object|MissyCriteria
: Search criteria -
options: Object?
: Driver-specific options, if supported.Options supported by all drivers:
multi: Boolean?
: Allow removing multiple entities. Inmulti=true
mode, the method returns an array. Default:true
Returns: a promise for a removed entity or null
when no matching entity is found.
When multi=true
, returns an array of entities.
// remove a single entityUser ; // remove multiple entitiesUser // remove teens ;
Chaining
Some Missy methods support chaining: the specified arguments are stashed for the future and used by the target method.
Currently, the following methods are supported: Model.get()
, Model.findOne()
, Model.find()
.
Model.pick(fields)
Stash the fields
argument for the next query.
User;
Model.sort(sort)
Stash the sort
argument for the next query.
User;
Model.skip(n)
Stash the skip
option for the next query.
User;
Model.limit(n)
Stash the limit
option for the next query.
User;
Using The Driver Directly
Missy search criteria is limited in order to keep the implementation simple. To make complex queries, you'll need to use the Driver directly, and optionally pass the returned entities through Missy.
In order to mimic Missy behavior, you need to do the following:
- Get the vanilla DB client object from the Schema or Model.
- Execute a query using the client
- Use Missy methods to preprocess the data. This includes the hooks!
MongoDB Example:
var Q = ; var client = User; // get the vanilla DB client // Load an entityQ ; // Save an entityvar entity = id: '0' age: '23' ; // wrong field types! User ;
Model Hooks
Missy Models allow you to hook into the internal processes, which allow you to preprocess the data, tune the internal behavior, or just tap the data.
Hooks are implemented with MissyHooks
which is instantiated under the Model.hooks
property of
every mode. It supports both synchronous hooks and event hooks.
You can assign synchronous hooks which are executed within the Missy and can interfere with the process:
// Add a hookUserhooks{ _;};
Also, you can subscribe to events named after the available hooks:
Userhooks;
Almost every Missy method is integrated with the hook system.
Converter Hooks
Allow you to hook into entityImport()
and entityExport()
functions and alter the way entities are preprocessed
while Missy talks to database driver.
Hook name | Arguments | Called in | Usage example |
---|---|---|---|
beforeImport |
entity:Object |
In entityImport() right after fetching the entity from the DB. |
Prepare values fetched from the DB |
afterImport |
entity:Object |
In entityImport() after the field type convertions are applied to the entity. |
Final tuning before the entity is returned to the user |
beforeExport |
entity:Object |
In entityExport() right after the entity is provided by the Missy user |
Sanitize the data |
afterExport |
entity:Object |
In entityExport() after the field type convertions are applied to the entity. |
Prepare the data before storing it to the DB ; Validation |
User{ delete entitypassword; // never expose the password};
Query Hooks
Allow you to hook into Missy query methods and alter the way these are executed, including all the input/output values.
All hooks receive the ctx: IModelContext
argument: an object that holds the current query context.
In hooks, you can modify its fields to alter the Missy behavior. ctx
fields set depends on the query, but in general:
ctx.model: Model
: The model the query is executed onctx.criteria: MissyCriteria?
: Search criteriactx.fields: MissyProjection?
: Fields projectionctx.sort: MissySort?
: Sortingctx.update: MissyUpdate?
: Update operationsctx.options: Object?
: Driver-dependent optionsctx.entities: Array.<Object>?
: The entities being handled (fetched or returned)
Each missy method invokes its own pair of before*
and after*
hooks.
The after*
hook is not invoked when an error occurs.
In addition, methods that accept/return entities invoke entityImport()
and entityExport()
on every entity,
which in turn triggers the converter hooks described above.
Hook name | Arguments | Model method |
---|---|---|
beforeFindOne |
entity:undefined, ctx: IModelContext |
findOne() |
afterFindOne |
entity:Object, ctx: IModelContext |
findOne() |
beforeFind |
entities:undefined, ctx: IModelContext |
find() |
afterFind |
entities:Array.<Object>, ctx: IModelContext |
find() |
beforeInsert |
entities:Array.<Object>, ctx: IModelContext |
insert() |
afterInsert |
entities:Array.<Object>, ctx: IModelContext |
insert() |
beforeUpdate |
entities:Array.<Object>, ctx: IModelContext |
update() |
afterUpdate |
entities:Array.<Object>, ctx: IModelContext |
update() |
beforeSave |
entities:Array.<Object>, ctx: IModelContext |
save() |
afterSave |
entities:Array.<Object>, ctx: IModelContext |
save() |
beforeRemove |
entities:Array.<Object>, ctx: IModelContext |
remove() |
afterRemove |
entities:Array.<Object>, ctx: IModelContext |
remove() |
beforeUpdateQuery |
entities:undefined, ctx: IModelContext |
updateQuery() |
afterUpdateQuery |
entities:Array.<Object>, ctx: IModelContext |
updateQuery() |
beforeRemoveQuery |
entities:undefined, ctx: IModelContext |
removeQuery() |
afterRemoveQuery |
entities:Array.<Object>, ctx: IModelContext |
removeQuery() |
Relations
Missy supports automatic loading & saving of related entities assigned to the host entities.
Defining Relations
Model.hasOne(prop, foreign, fields)
Define a 1-1 or N-1 relation to a foreign Model foreign
, stored in the local field prop
.
Arguments:
-
prop: String
: Name of the local property to handle the related entity in. This also becomes the name of the relation. -
foreign: Model
: The foreign Model -
fields: String|Array.<String>|Object
: Name of the common Primary Key field, or an array of common fields, or an object with the fields' mapping:Article;Article;
After a relation was defined, the local model's prop
field will be used for loading & saving the related entity.
Model.hasMany(prop, foreign, fields)
Define a 1-N relation to a foreign Model.
Same as hasOne
, but handles an array of related entities.
Handling Related Entities
Model.loadRelated(entities, prop, fields?, sort?, options?):Q
For the given entities, load their related entities as defined by the prop
relation.
Arguments:
-
entities: Object|Array.<Object>
: Entity of the current model, or an array of them -
prop: String|Array.<String>|undefined
: The relation name to load, or multiple relation names as an array.When
undefined
is given, all available relations are loaded.You can also load nested relations using the '.'-notation:
'articles.comments'
(see Model.withRelated). -
fields: String|Object|MissyProjection?
: Fields projection for the related entities. Optional.With the help of this field, you can load partial related entities.
-
sort: String|Object|Array|MissySort?
: Sort specification for the related entities. Optional. -
options: Object?
: Driver-dependent options for the relatedModel.find()
method. Optional.
Relations are effectively loaded with a single query per relation, irrespective to the number of host entities.
After the method is executed, all entities
will have the prop
property populated with the related entities:
- For
hasOne
, this is a single entity, orundefined
when no related entity exists. - For
hasMany
, this is always an array, possibly - empty.
Model.saveRelated(entities, prop, options?):Q
For the given entities, save their related entities as defined by the prop
relation.
Arguments:
-
entities: Object|Array.<Object>
: Entity of the current model, or an array of them -
prop: String|Array.<String>|undefined
: The relation name to save, or multiple relation names as an array.When
undefined
is given, all available relations are saved.You can also save nested relations using the '.'-notation:
'articles.comments'
(see Model.withRelated). -
options: Object?
: Driver-dependent options for the relatedModel.save()
method. Optional.
This method automatically sets the foreign keys on the related entities and saves them to the DB.
Model.removeRelated(entities, prop, options?):Q
For the given entities, remove their related entities as defined by the prop
relation.
Arguments:
entities: Object|Array.<Object>
: Entity of the current model, or an array of themprop: String|Array.<String>|undefined
: (same as above)options: Object?
: Driver-dependent options for the relatedModel.removeQuery()
method. Optional.
This method removes all entities that are related to this one with the specified relation.
Model.withRelated(prop, ...):Model
Automatically process the related entities with the next query:
-
- find(), findOne(): load related entities
-
- insert(), update(), save(): save related entities (replaces them & removes the missing ones)
-
- remove(): remove related entities
In fact, this method just stashes the arguments for loadRelated(), saveRelated(), removeRelated(), and calls the corresponding method in the subsequent query:
Model.withRelated(prop, fields, sort, options)
when going to load entitiesModel.withRelated(prop, options)
when going to save entitiesModel.withRelated(prop, options)
when going to remove entities
See examples below.
Example
For instance, having the following schema:
var User = schema; var Article = schema; var Comment = schema; // Define relationsUser;Article; Article;Comment;Comment;
Saving Related Rntities
User // process the named relation in the subsequent query ;
Loading Related Rntities
User // load articles into the `articles` field // load comments for each article, sorted (nested relation) ;
Removing Related Rntities
User ;
Recipes
Validation
Missy does not support any validation out of the box, but you're free to choose any external validation engine.
The current approach is to install the validation procedure into the beforeExport
Model hook
and check entities that are saved or updated. Note that this approach won't validate entities modified with
Model.updateQuery, as this method does not handle full entities!
An example is on its way.