Install it with npm:
$ npm install --save firenze
Or Bower:
$ bower install --save firenze
firenze.js is a adapter-based object relational mapper targetting node.js, io.js and the browser.
Key features:
- Written in ES6 (works in both node/io.js/browser)
- Adapter based structure to plug in any database
- Promise based workflow
- Strong validation support
The project is still in heavy development, and more features are expected to land in future releases.
Available adapters:
Terminologies:
Terminologies for developing with firenze.js can be broken down into a handful of items:
- Database
- Adapter
- Collection
- Model
Each of them are discussed in the documentation below.
- Install
- Quickstart
- Database
- Adapter
- Collection
- Models
- Testing
- License
firenze.js can be used in both node.js/io.js, as well as the browser.
$ npm install --save firenze
Now you can require it as follows:
var firenze = require('firenze');
Installation is same as Node.js since npm
is the common package manager.
To import:
import firenze from 'firenze';
You can download firenze.js using Bower.
$ bower install --save firenze
The package comes with multiple dist files, and you are free to choose whatever workflow suits you best.
If you want to include just one file alone with all the dependencies:
<script src="bower_components/firenze/dist/firenze.full.min.js"></script>
<script>
// the library is now available in `firenze` variable
</script>
If you wish to include only the core library, and load its dependencies manually:
<script src="bower_components/lodash/lodash.min.js"></script>
<script src="bower_components/async/lib/async.js"></script>
<script src="bower_components/bluebird/js/browser/bluebird.min.js"></script>
<script src="bower_components/validator-js/validator.min.js"></script>
<script src="bower_components/firenze/dist/firenze.min.js"></script>
<script>
// use `firenze` variable to access the library
</script>
The example is targetting Node.js environment.
Install the module (along with an adapter) first:
$ npm install --save firenze firenze-adapter-mysql
Now you can proceed to managing your database as folllows:
var f = require('firenze');
var Database = f.Database;
var MysqlAdapter = require('firenze-adapter-mysql');
// create an instance of your Database
var db = new Database({
adapter: MysqlAdapter,
host: '127.0.0.1',
database: 'my_database',
user: '',
password: ''
});
// define a Collection, which represents a table
var Posts = db.createCollectionClass({ // or db.Collection()
table: 'posts',
modelClass: function () {
return Post;
}
// or modelClass: Post
});
// define a Model, which represents a record
var Post = db.createModelClass({ // or db.Model()
alias: 'Post',
collectionClass: Posts, // or a function that returns Posts
schema: {
id: {
type: 'integer'
},
title: {
type: 'string'
},
body: {
type: 'text'
}
}
});
// finding
var posts = new Posts();
posts.find('first', {
conditions: {
id: 1
// can also be prefixed with Model alias as:
// 'Post.id': 1
}
}).then(function (post) {
// post in an instance of Model, with fetched data
var title = post.get('title');
// or convert to plain object
var postObject = post.toObject();
var title = postObject.title;
});
// saving
var post = new Post({
title: 'Hello World',
body: 'blah...'
});
post.save().then(function (model) {
console.log('Saved with ID: ' + model.get('id'));
});
Before anything else, you need to create an instance of Database
with your credentials which will be referenced in your Collections and Models.
var f = require('firenze');
var Database = f.Database;
var MysqlAdapter = require('firenze-adapter-mysql');
var db = new Database({
adapter: MysqlAdapter,
host: '127.0.0.1',
database: 'my_database',
user: '',
password: '',
pool: {
min: 0,
max: 1
}
});
Also aliased as .Collection(extend)
.
Also aliased as .Model(extend)
Returns adapter
Returns connection of the Adapter
Closes the connection
Adapter is responsible for making the actual database operations.
You would hardly ever need to create an instance of a Adapter. Database class would take care of it.
An adapter instance is created with the same options passed when creating a Database instance:
For example, if you are using MySQL adapter, it would be like this:
$ npm install --save firenze-adapter-mysql
Now let's create an instance of Database:
var f = require('firenze');
var Database = f.Database;
var MysqlAdapter = require('firenze-adapter-mysql');
var db = new Database({
adapter: MysqlAdapter,
host: '127.0.0.1',
database: 'my_database',
user: '',
password: ''
});
Every adapter needs to implement at least these methods below:
Returns the current connection
Closes the current connection, and calls the callback function cb()
if passed.
Gets a query object
Creates a new record
Fetches the results found against the query object
Updates the records matching againt query object with given data
Deletes the records matching against query object
Drop table if exists
Create table based on model's schema
Insert rows into model's table
Creates table, and loads data for given model
Runs fixtures for multiple models
arr = [{model: post, rows: rows}]
A collection represents a table. If you have a posts
table, most likely you would have a collection for it called Posts
.
You can create a Collection class from your Database instance. And it requires minimum two properies, table
, and modelClass
:
var Posts = db.createCollectionClass({
table: 'posts',
modelClass: function () {
return Post;
}
});
There is also a short method for creating Collection class via db.Collection()
.
You can also create a Collection class like this:
var Posts = f.createCollectionClass({
db: db, // instance of your Database
table: 'posts',
// ...
});
If you are using ES6:
class Posts extends f.Collection {
constructor(extend = {}) {
super(extend);
this.setDatabase(db);
}
}
Every collection requires a Model for representing its records. This property can directly reference to the Model class, or it can be a function that returns the Model class.
The name of the table that this Collection represents. Always as a string.
List of mapped finder methods that you want available in .find(mappedName, options)
By default these are set:
{
all: 'findAll',
first: 'findFirst',
count: 'findCount',
list: 'findList'
}
This mapping allows you to later call .find('all', options)
, which eventually calls .findAll(options)
.
Before using the Collection, you need to create an instance of it:
var posts = new Posts();
Get an instance of this Collection's model
Get in instance of the current Database
Get adapter of the Collections' database
Change database instance of this Collection to db
Get query object for this Collection
Explained above in finders
section
Returns a promise with matched results.
Same as collection.find('all', options)
.
Returns a promise with matched model if any.
Same as collection.find('first', options)
.
Returns a promise with count of matched results.
Same as collection.find('count', options)
.
Returns a promise with key/value pair of matched results.
Same as collection.find('list', options)
.
Save the given model. This method is not usually called directly, but rather via Model.save()
.
Returns a promise with model instance.
Deletes the given model. Usually called via Model.delete()
.
Returns a promise.
A model represents a record of a table. If you have a posts
table, most likely you would want to name your Model class in its singular for, which is Post
.
You can create a Model class from your Database instance. And it can be created as follows:
var Post = db.createModelClas({
alias: 'Post',
displayField: 'title',
schema: {
id: {
type: 'increments'
},
title: {
type: 'string'
}
},
collectionClass: Posts
});
There is a short method for creating a Model class via db.Model()
.
You can also create a Model class like this:
var Post = f.createModelClass({
// ...
});
If you are using ES6:
class Post extends f.Model {
constructor(attributes = {}, extend = {}) {
super(attributes, extend);
}
}
Just like how Collection has a modelClass, models also need to have a collectionClass. It can be a direct reference to the class, or it can be a function that returns the class.
Models do not necessarily need to define their full schema, but you would need them for building fixtures and also assigning validation rules for example later.
The keys of this object are the column names, and the value defines what type of column they are. For example:
{
id: {
type: 'integer'
},
title: {
type: 'string'
}
}
Column types can vary depending on the adapter you are using.
You also use the schema
property to set validation rules.
For example:
{
email: {
type: 'string',
validate: {
rule: 'isEmail',
message: 'Please enter a valid email address'
}
}
}
Validations will be discussed further later in its own section.
Your model's data
The name of the ID field, defaults to id
.
This is the field that represents your record's display value. Usually title
or name
in most cases.
For convenience, stores the ID of the model in this property
Example:
{
ruleName: function (field, value) {
return true;
},
asyncRule: function (value, field, validated) {
return validated(true);
},
ruleWithOptions: function (value, field, arg1, arg2) {
return true;
}
}
Unless defined, alias always defaults to the table name as defined in the Collection class of a Model. When associations get in the way, having a unique alias helps avoiding ambiguity when constructing complex conditions.
Unless otherwise you are already provided with a model instance from a Collection, you need to create an instance of it:
var post = new Post();
You can also create an instance of a Model with some data:
var post = new Post({
title: 'Hello World',
body: 'blah...'
});
Validation rules for fields can be set when defining the schema:
db.createModelClass({
schema: {
email: {
type: 'string',
validate: {
rule: 'isEmail',
message: 'Please enter a valid email address'
}
}
}
});
{
email: {
type: 'string',
validate: [
{
rule: 'isLowercase',
message: 'Please enter email address in lowercase',
},
{
rule: 'isEmail',
message: 'Please enter a valid email address'
}
]
}
}
{
fruit: {
type: 'string',
validate: {
rule: [
'isIn', // `isIn` is the rule name
[
'apple',
'banana'
] // this array is passed as an argument to rule function
],
message: 'Must be either apple or banana'
}
}
}
{
mood: {
type: 'string',
validate: {
rule: function (field, value) {
return true;
}
}
}
}
{
food: {
type: 'string',
validate: {
rule: function (field, value, done) {
checkIfFoodIsHealthy(value, function (healthy) {
var isHealthy = healthy === true;
done(isHealthy);
});
}
}
}
}
By default, all the validation rules from Validator.js is available:
- equals(str, comparison) - check if the string matches the comparison.
- contains(str, seed) - check if the string contains the seed.
- matches(str, pattern [, modifiers]) - check if string matches the pattern. Either
matches('foo', /foo/i)
ormatches('foo', 'foo', 'i')
. - isEmail(str [, options]) - check if the string is an email.
options
is an object which defaults to{ allow_display_name: false, allow_utf8_local_part: true }
. Ifallow_display_name
is set to true, the validator will also matchDisplay Name <email-address>
. Ifallow_utf8_local_part
is set to false, the validator will not allow any non-English UTF8 character in email address' local part. - isURL(str [, options]) - check if the string is an URL.
options
is an object which defaults to{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false }
. - isFQDN(str [, options]) - check if the string is a fully qualified domain name (e.g. domain.com).
options
is an object which defaults to{ require_tld: true, allow_underscores: false, allow_trailing_dot: false }
. - isIP(str [, version]) - check if the string is an IP (version 4 or 6).
- isAlpha(str) - check if the string contains only letters (a-zA-Z).
- isNumeric(str) - check if the string contains only numbers.
- isAlphanumeric(str) - check if the string contains only letters and numbers.
- isBase64(str) - check if a string is base64 encoded.
- isHexadecimal(str) - check if the string is a hexadecimal number.
- isHexColor(str) - check if the string is a hexadecimal color.
- isLowercase(str) - check if the string is lowercase.
- isUppercase(str) - check if the string is uppercase.
- isInt(str [, options]) - check if the string is an integer.
options
is an object which can contain the keysmin
and/ormax
to check the integer is within boundaries (e.g.{ min: 10, max: 99 }
). - isFloat(str [, options]) - check if the string is a float.
options
is an object which can contain the keysmin
and/ormax
to validate the float is within boundaries (e.g.{ min: 7.22, max: 9.55 }
). - isDivisibleBy(str, number) - check if the string is a number that's divisible by another.
- isNull(str) - check if the string is null.
- isLength(str, min [, max]) - check if the string's length falls in a range. Note: this function takes into account surrogate pairs.
- isByteLength(str, min [, max]) - check if the string's length (in bytes) falls in a range.
- isUUID(str [, version]) - check if the string is a UUID (version 3, 4 or 5).
- isDate(str) - check if the string is a date.
- isAfter(str [, date]) - check if the string is a date that's after the specified date (defaults to now).
- isBefore(str [, date]) - check if the string is a date that's before the specified date.
- isIn(str, values) - check if the string is in a array of allowed values.
- isCreditCard(str) - check if the string is a credit card.
- isISIN(str) - check if the string is an [ISIN][ISIN] (stock/security identifier).
- isISBN(str [, version]) - check if the string is an ISBN (version 10 or 13).
- isMobilePhone(str, locale) - check if the string is a mobile phone number, (locale is one of
['zh-CN', 'en-ZA', 'en-AU', 'en-HK', 'pt-PT', 'fr-FR', 'el-GR', 'en-GB', 'en-US', 'en-ZM']
). - isJSON(str) - check if the string is valid JSON (note: uses JSON.parse).
- isMultibyte(str) - check if the string contains one or more multibyte chars.
- isAscii(str) - check if the string contains ASCII chars only.
- isFullWidth(str) - check if the string contains any full-width chars.
- isHalfWidth(str) - check if the string contains any half-width chars.
- isVariableWidth(str) - check if the string contains a mixture of full and half-width chars.
- isSurrogatePair(str) - check if the string contains any surrogate pairs chars.
- isMongoId(str) - check if the string is a valid hex-encoded representation of a [MongoDB ObjectId][mongoid].
- isCurrency(str, options) - check if the string is a valid currency amount.
options
is an object which defaults to{symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_space_after_digits: false }
.
Example usage of the above mentioned rules:
db.createModelClass({
schema: {
title: {
// direct rule
validate: {
rule: 'isAlphanumeric'
}
},
body: {
// rule with options
validate: {
rule: ['isLength', min, max]
}
}
}
});
But of course, you can always override them or add new custom rules.
Validation rules can be defined when creating a Model class:
var Post = db.createModelClass({
schema: {
name: {
type: 'string',
validate: {
rule: 'myFirstRule'
}
},
title: {
type: 'string',
validate: {
rule: [
'myRuleWithOptions',
'arg1 value',
'arg2 value'
]
}
}
},
validationRules: {
myFirstRule: function (field, value) {
return true; // validated successfully
},
myRuleWithOptions: function (field, value, arg1, arg2) {
return true;
},
myAsyncRule: function (field, value, done) {
doSomething(value, function (result) {
var validated = result === true;
done(validated);
});
}
}
});
By default, validation rules are only checked against fields that are set.
But if you wish to make sure that certain fields are required, meaning they should always be present, you can mark them as required in your schema:
var Post = db.createModelClass({
schema: {
name: {
type: 'string',
validate: {
rule: 'isAlpha',
required: true,
message: 'Must be alphabets only'
}
}
}
});
Get the model's Collection's instance
Get the field of current model
Set an attribute with given value for the field
Returns a plain object of the model
Alias of .toObject()
.
Fetches the model again from the Database, and returns it with a promise.
A quick example:
var post = new Post({id: 1});
post.fetch().then(function (model) {
var title = model.get('title');
});
Get the ID of model
Is the current model new? As in saved in Database, or yet to be saved?
Save the current model, and returns a promise.
Options:
callbacks
: Defaults to true, pass false to disable before/after callbacks.
Save a particular field with value.
Returns a promise.
Clear the current instance of model of any data
Delete the current model, and return a promise.
Options:
callbacks
: Defaults to true, pass false to disable before/after callbacks.
Validates all fields of the current Model
Returns a promise with true
if all validated, otherwise an object of error messages keyed by field names.
@TODO: reject()
instead on error?
Options:
callbacks
: Defaults to true, pass false to disable before/after callbacks.
Validates a single field
Returns a promise with true if validated, otherwise error message
Models also support callbacks that you can define when creating classes.
For example:
var Promise = f.Promise;
var Post = f.createModelClass({
alias: 'Post',
beforeSave: function () {
// do something before saving...
// end the callback with a promise
return new Promise.resolve(true);
}
});
Should return a Promise with true
to continue.
To stop the save, return a Promise with an error.
Should return a Promise.
Should return a Promise with true
to continue.
To stop the validation, return a Promise with an error.
Should return a Promise.
Should return a Promise with true
to continue.
To stop from deleting, return a Promise with an error.
Should return a Promise.
Tests are written with mocha, and can be run via npm:
$ npm test
MIT © Fahad Ibnay Heylaal