bookshelf-schema¶
The Bookshelf plugin that adds fields, relations, scopes and more to bookshelf models.
Contents:¶
Basic usage¶
CoffeeScript¶
Enable plugin:
Schema = require 'bookshelf-schema'
knex = require('knex')({...})
db = require('bookshelf')(knex)
db.plugin Schema({...})
Define model:
{StringField, EmailField} = require 'bookshelf-schema/lib/fields'
{HasMany} = require 'bookshelf-schema/lib/relations'
Photo = require './photo'
class User extends db.Model
tableName: 'users'
@schema [
StringField 'username'
EmailField 'email'
HasMany Photo
]
JavaScript¶
Enable plugin:
var Schema = require('bookshelf-schema');
var knex = require('knex')({...});
var db = require('bookshelf')(knex);
db.plugin(Schema({...}));
Define model:
var Fields = require('bookshelf-schema/lib/fields'),
StringField = Fields.StringField,
EmailField = Fields.EmailField;
var Relations = require('bookshelf-schema/lib/relations'),
HasMany = Relations.HasMany;
var Photo = require('./photo');
var User = db.Model.extend({ tableName: 'users' }, {
schema: [
StringField('username'),
EmailField('email'),
HasMany(Photo)
]
});
Schema definition¶
Schema passed to db.Model.schema
method or to a “schema” static field is an array of “schema
entities”. Each of that entity class defines special methods used in process of augementing and
initiaizing model.
The bookshelf-schema comes with several predefined classes adding fields, relations, scopes etc. You may see some of them in examples above: StringField, EmailField, HasMany.
You may define your own schema entities with custom behaviour.
Fields¶
Note
Exported from bookshelf-schema/lib/fields
Fields enhances models in several ways:
- each field adds an accessor property so instead of calling
model.get('fieldName')
you may usemodel.fieldName
directly - each field may convert data when model is parsed or formatted
- model may use field-specific validation before save or explicitly. Validation uses the Checkit module.
Examples¶
CoffeeScript¶
{StringField, EncryptedStringField} = require 'bookshelf-schema/lib/fields'
class User extends db.Model
tableName: 'users'
@schema [
StringField 'username', required: true
EncryptedStringField 'password', minLength: 8
]
User.forge(username: 'alice', password: 'secret-password').save() # [1]
.then (alice) ->
User.forge(id: alice.id).fetch()
.then (alice) ->
alice.username.should.equal 'alice' # [2]
alice.password.verify('secret-password').should.become.true # [3]
JavaScript¶
var Fields = require('bookshelf-schema/lib/fields');
var StringField = Fields.StringField;
var EncryptedStringField = Fields.EncryptedStringField;
var User = db.Model.extend( { tableName: 'users' }, {
schema: [
StringField('username', {required: true}),
EncryptedStringField('password', {minLength: 8})
]
});
User.forge({username: 'alice', password: 'secret-password'}).save() // [1]
.then( function(alice) {
return User.forge({id: alice.id}).fetch()
}).then( function(aclice) {
alice.username.should.equal('alice'); // [2]
alice.password.verify('secret-password').should.become.true; // [3]
});
- [1]: model is validated before save
- [2]: alice.get(‘username’) is called internally
- [3]: password field is converted to special object when fetched from database.
Validation¶
-
Model.prototype.
validate
()¶ Returns: Promise[Checkit.Error]
Model method validate is called automatically before saving or may be called explicitly. It takes validation rules added to model by fields and passes them to Checkit.
You may override this method in your model to add custom validation logic.
Base class¶
All fields are a subclass of Field class.
-
class
Field
(name, options = {})¶ Arguments: - name (String) – the name of the field
- options (Object) – field options
Options:
- column: String
- use passed string as a database column name instead of field name
- createProperty: Boolean, default true
- create accessor for this field
- validation: Boolean, default true
- enable validation of this field value
- message: String
- used as a default error message
- label: String
- used as a field label when formatting error messages
- validations: Array
- array of validation rules that Checkit can understand
Field classes¶
StringField¶
-
class
StringField
(name, options = {})¶
Options:
- minLength | min_length: Integer
- validate field value length is not lesser than minLength value
- maxLength | max_length: Integer
- validate field value length is not greater than maxLength value
EmailField¶
-
class
EmailField
(name, options = {})¶
Like a StringField with simple check that value looks like a email address.
UUIDField¶
-
class
UUIDField
(name, options = {})¶
Like as StringField that should be formatted as a UUID.
EncryptedStringField¶
-
class
EncryptedStringField
(name, options = {})¶
Options:
- algorithm: String | Function
Function: function that will take string, salt, iteration count and key length as an arguments and return Promise with encrypted value
String: algorithm name passed to crypto.pbkdf2
- iterations: Integer
- iterations count passed to encryption function
- keylen: Integer
- key length passed to encryption function
- saltLength: Integer, default 16
- salt length in bytes
- saltAlgorithm: Function
- function used to generate salt. Should take salt length as a parameter and return a Promise with salt value
- minLength | min_length: Integer
- validate that unencrypted field value length is not lesser than minLength value checked only when unencrypted value available
- maxLength | max_length: Integer
- validate that unencrypted field value length is not greater than maxLength value checked only when unencrypted value available
-
class
EncryptedString
()¶ Internal class used to handle encrypted value.
EncryptedStringField value became EncryptedString when saved. It looses it’s plain value.
You should use method verify(value) : Promise
to verify value against saved string.
NumberField¶
-
class
NumberField
(name, options = {})¶
Options:
- greaterThan | greater_than | gt: Number
- validates that field value is greater than option value
- greaterThanEqualTo | greater_than_equal_to | gte | min: Number
- validates that field value is not lesser than option value
- lessThan | less_than | lt: Number
- validates that field value is lesser than option value
- lessThanEqualTo | less_than_equal_to | lte | max: Number
- validates that field value is not greater than option value
IntField¶
-
class
IntField
(name, options = {})¶
NumberField checked to be an Integer.
Options (in addition to options from NumberField):
- naturalNonZero | positive: Boolean
- validates that field value is positive
- natural: Boolean
- validates that field value is positive or zero
Advanced validation¶
you may assign object instead of value to validation options:
minLength: {value: 10, message: '{{label}} is too short to be valid!'}
you may add complete Checkit validation rules to field with validations option:
StringField 'username', validations: [{rule: 'minLength:5'}]
Relations¶
Note
Exported from bookshelf-schema/lib/relations
Relations are used to declare relations between models.
When applied to model it will
- create function that returns appropriate model or collection, like you normally does when define relations for bookshelf models
- create accessor prefixed by ‘$’ symbol (may be configured)
- may prevent destroying of parent model or react by cascade destroying of related models or detaching them
Examples¶
CoffeeScript¶
{HasMany} = require 'bookshelf-schema/lib/relations'
class Photo extends db.Model
tableName: 'photos'
class User extends db.Model
tableName: 'users'
@schema [
StringField 'username'
HasMany Photo, onDestroy: 'cascade' # [1]
]
User.forge(username: 'alice').fetch()
.then (alice) ->
alice.load('photos') # [2]
.then (alice) ->
alice.$photos.at(0).should.be.an.instanceof Photo # [3]
JavaScript¶
var Relations = require('bookshelf-schema/lib/relations');
var HasMany = Relations.HasMany;
var Photo = db.Model.extend({ tableName: 'photos' });
var User = db.Model.extend({ tableName: 'users' }, {
schema: [
StringField('username'),
HasMany(Photo, {onDestroy: 'cascade'}) // [1]
]
});
User.forge({username: 'alice'}).fetch()
.then( function(alice) {
return alice.load('photos'); // [2]
}).then ( function(alice) {
alice.$photos.at(0).should.be.an.instanceof(Photo); // [3]
});
[1] HasMany will infer relation name from the name of related model and set it to ‘photos’
When relation name is generated from model name it uses model name with lower first letter and pluralize it for multiple relations.
[1] when used with registry plugin you may use model name instead of class. It will be resolved in a lazy manner.
[2] load will work like in vanilla bookshelf thanks to auto-generated method ‘photos’
[3] $photos internally calls
alice.related('photos')
and returns fetched collection
Relation name¶
Actual relation name (the name of generated function) is generated from one of the following, sequently:
- name passed as an option to relation constructor
- string, passed as a relation if used with registry
- relatedModel.name or relatedModel.displayName
- camelized and singularized related table name
Additionally if name isn’t passed as an option relation name is pluralized for the multiple relations and its first letter is converted to lower case.
Accessor helper methods¶
In addition to common collection or model methods accessors provides several helpers:
-
assign
(list, options = {})¶ Arguments: - list (Array) – list of related models, ids, or plain objects
- options (Object) – options passed to save methods
alice.$photos.assign([ ... ])
Assigns passed objects to relation. All related models that doesn’t included to passed list will be detached. It will fetch passed ids and tries to creates new models for passed plain objects.
For singular relations such as HasOne or BelongsTo it accepts one object instead of list.
-
attach
(list, options = {})¶ alice.$photos.attach([ ... ])
Similar to assign but only attaches objects.
-
detach(list, options = {}
()¶ alice.$photos.detach([ ... ])
Similar to assign but only detaches objects. Obviously it can’t detach plain objects.
Note
assign, attach and detach are wrapped with transaction
Count¶
-
Collection.prototype.
count
()¶
Bookshelf Collection.prototype.count method is replaced and now (finally!) usable with relations and scoped collections. So you can do something like alice.$photos.count().then (photosCount) -> ...
And it still passes all the count-related tests provided by Bookshelf.
Base class¶
All relations are a subclass of Relation class.
-
class
Relation
(model, options = {})¶ Arguments: - model ((Class|String)) – related model class. Could be a string if used with registry plugin.
- options (Object) – relation options
Options:
- createProperty: Boolean, default true
- create accessors for this relation
- accessorPrefix: String, default “$”
- used to generate name of accessor property
- onDestroy: String, one of “ignore”, “cascade”, “reject”, “detach”, default “ignore”
determines what to do when parend model gets destroyed
- ignore - do nothing
- cascade - destroy related models
- reject - prevent parent model destruction if there is related models
- detach - detach related models first
Note
Model.destroy is patched so it will wrap callbacks and actual model destroy with transaction
- through: (Class|String)
- generate “through” relation
- foreignKey, otherKey, foreignKeyTarget, otherKeyTarget, throughForeignKey, throughForeignKeyTarget: String
- has the same meaning as in appropriate Bookshelf relations
Relation classes¶
BelongsTo¶
-
class
BelongsTo
(model, options = {})¶
Adds IntField <name>_id to model schema
Note
if custom foreignKey used it may be necessary to explicitly add corresponding field to avoid validation errors
MorphOne¶
-
class
MorphOne
(model, polymorphicName, options = {})¶ Arguments: - polymorphicName (String) –
Options:
- columnNames: [String, String]
- First is a database column for related id, second - for related type
- morphValue: String, defaults to target model tablename
- The string value associated with this relation.
MorphMany¶
-
class
MorphMany
(model, polymorphicName, options = {})¶ Arguments: - polymorphicName (String) –
Options:
- columnNames: [String, String]
- First is a database column for related id, second - for related type
- morphValue: String, defaults to target model tablename
- The string value associated with this relation.
MorphTo¶
-
class
MorphTo
(polymorphicName, targets, options = {})¶ Arguments: - polymorphicName (String) –
- targets (Array) – list of target models
Options:
- columnNames: [String, String]
- First is a database column for related id, second - for related type
Adds IntField <name>_id or columnNames[0] to model schema
Adds StringField <name>_type of columnNames[1] to model schema
Scopes¶
Note
Exported from bookshelf-schema/lib/scopes
Adds rails-like scopes to model.
Examples¶
CoffeeScript¶
Scope = require 'bookshelf-schema/lib/scopes'
class User extends db.Model
tableName: 'users'
@schema [
StringField 'username'
BooleanField 'flag'
Scope 'flagged', -> @where flag: true # [1]
Scope 'nameStartsWith', (prefix) -> # [2]
@where 'username', 'like', "#{prefix}%"
]
class Group extends db.Model
tableName: 'groups'
@schema [
BelongsToMany User
]
User.flagged().fetchAll()
.then (flaggedUsers) ->
flaggedUsers.all('flag').should.be.true
User.flagger().nameStartsWith('a').fetchAll() # [3]
.then (users) ->
users.all('flag').should.be.true
users.all( (u) -> u.username[0] is 'a' ).should.be.true
Group.forge(name: 'users').fetch()
.then (group) ->
group.$users.flagged().fetch() # [4]
.then (flaggedUsers) ->
flaggedUsers.all('flag').should.be.true
JavaScript¶
var Scope = require('bookshelf-schema/lib/scopes');
var User = db.Model.extend( { tableName: 'users' }, {
schema: [
StringField('username'),
BooleanField('flag'),
Scope('flagged', function(){ // [1]
this.where({ flag: true });
}),
Scope('nameStartsWith', function(prefix) { // [2]
this.where('username', 'like', prefix + '%')
})
]
});
var Group = db.Model.extend( { tableName: 'groups' }, {
schema: [ BelongsToMany(User) ]
});
User.flagged().fetchAll()
.then( function(flaggedUsers) {
flaggedUsers.all('flag').should.be.true;
});
User.flagged().nameStartsWith('a').fetchAll() // [3]
.then( function(users) {
users.all('flag').should.be.true;
users.all(function(u){
return u.username[0] == 'a';
}).should.be.true;
});
Group.forge({ name: 'users' }).fetch()
.then( function(group) {
return group.$users.flagged().fetch() // [4]
}).then( function(flaggedUsers) {
flaggedUsers.all('flag').should.be.true;
});
- [1]: scope invoked in context of query builder, not model
- [2]: scopes are just a functions and may use an arguments
- [3]: scopes may be chained
- [4]: scopes from target model are automatically lifted to relation
Base class¶
-
class
Scope
(name, builder)¶ Arguments: - name (String) – scope name
- builder (Function) – scope function
Default scope¶
Scope with name “default” is automatically applied when model is fetched from database.
Listen¶
Note
Exported from bookshelf-schema/lib/listen
Declare event listener.
Examples¶
CoffeeScript¶
Listen = require 'bookshelf-schema/lib/listen'
class User extends db.Model
tableName: 'users'
@schema [
Listen 'saved', ( -> console.log "#{@username} saved" )
Listen 'fetched', 'onFetched'
]
onFetched: -> console.log "#{@username} fetched"
JavaScript¶
var Listen = require('bookshelf-schema/lib/listen');
var User = db.Model.extend( {
tableName: 'users',
onFetched: function() {
console.log this.username + ' fetched';
}
}, {
schema: [
Listen('saved', function(){ console.log( this.username + ' saved'); }),
Listen('fetched', 'onFetched')
]
});
Callbacks are called in context of model instance. If callback is a string it should be a model method name.
Options¶
Note
Exported from bookshelf-schema/lib/options
Sets plugin options for specific model
Examples¶
CoffeeScript¶
Options = require 'bookshelf-schema/lib/options'
class User extends db.Model
tableName: 'users'
@schema [
Options validation: false # [1]
]
JavaScript¶
var Options = require('bookshelf-schema/lib/options')
var User = db.Model.extend({ tableName: 'users' }, {
schema: [ Options({ validation: false }) ] // [1]
});
- [1] disable validation for model User