Thanks to visit codestin.com
Credit goes to github.com

Skip to content

lifeIsGame/objection.js

 
 

Repository files navigation

Build Status Coverage Status

Introduction

Objection.js is an ORM for Node.js that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine.

Objection.js is built on the wonderful SQL query builder knex. All databases supported by knex are supported by objection.js. SQLite3, Postgres and MySQL are thoroughly tested.

What objection.js gives you:

What objection.js doesn't give you:

  • A custom query DSL. SQL is used as a query language.
  • Automatic database schema creation and migration. It is useful for the simple things, but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things to you. knex has a great migration tool that we recommend for this job. Check out the example project.

Objection.js uses Promises and coding practices that make it ready for the future. We use Well known OOP techniques and ES6 compatible classes and inheritance in the codebase. You can even use things like ES7 async/await using a transpiler such as Babel. Check out our ES6 and ES7 example projects.

Topics

Installation

npm install knex objection

You also need to install one of the following depending on the database you want to use:

npm install pg
npm install sqlite3
npm install mysql
npm install mysql2
npm install mariasql

Getting started

To use objection.js all you need to do is initialize knex and give the connection to objection.js using Model.knex(knex):

var Knex = require('knex');
var Model = require('objection').Model;

var knex = Knex({
  client: 'postgres',
  connection: {
    host: '127.0.0.1',
    database: 'your_database'
  }
});

Model.knex(knex);

The next step is to create some migrations and models and start using objection.js. The best way to get started is to use the example project:

git clone [email protected]:Vincit/objection.js.git objection
cd objection/examples/express
npm install
# We use knex for migrations in this example.
npm install knex -g
knex migrate:latest
npm start

The express example project is a simple express server. The example-requests.sh file contains a bunch of curl commands for you to start playing with the REST API.

cat example-requests.sh

If you are using a newer version of Node and you want to use ES6 features then our ES6 version of the example project is the best place to start.

We also have an ES7 version of the example project that uses Babel for ES7 --> ES5 transpiling.

Also check out our API documentation and recipe book.

Query examples

The Person model used in the examples is defined here.

All queries are started with one of the Model methods query(), $query() or $relatedQuery(). All these methods return a QueryBuilder instance that can be used just like a knex QueryBuilder.

Insert a person to the database:

Person
  .query()
  .insert({firstName: 'Jennifer', lastName: 'Lawrence'})
  .then(function (jennifer) {
    console.log(jennifer instanceof Person); // --> true
    console.log(jennifer.firstName); // --> 'Jennifer'
    console.log(jennifer.fullName()); // --> 'Jennifer Lawrence'
  })
  .catch(function (err) {
    console.log('oh noes');
  });
insert into "Person" ("firstName", "lastName") values ('Jennifer', 'Lawrence')

Fetch all persons from the database:

Person
  .query()
  .then(function (persons) {
    console.log(persons[0] instanceof Person); // --> true
    console.log('there are', persons.length, 'Persons in total');
  })
  .catch(function (err) {
    console.log('oh noes');
  });
select * from "Person"

The return value of the .query() method is an instance of QueryBuilder that has all the methods a knex QueryBuilder has. Here is a simple example that uses some of them:

Person
  .query()
  .where('age', '>', 40)
  .andWhere('age', '<', 60)
  .andWhere('firstName', 'Jennifer')
  .orderBy('lastName')
  .then(function (middleAgedJennifers) {
    console.log('The last name of the first middle aged Jennifer is');
    console.log(middleAgedJennifers[0].lastName);
  });
select * from "Person"
where "age" > 40
and "age" < 60
and "firstName" = 'Jennifer'
order by "lastName" asc

The next example shows how easy it is to build complex queries:

Person
  .query()
  .select('Person.*', 'Parent.firstName as parentFirstName')
  .join('Person as Parent', 'Person.parentId', 'Parent.id')
  .where('Person.age', '<', Person.query().avg('Person.age'))
  .whereExists(Animal.query().select(1).whereRef('Person.id', 'Animal.ownerId'))
  .orderBy('Person.lastName')
  .then(function (persons) {
    console.log(persons[0].parentFirstName);
  });
select "Person".*, "Parent"."firstName" as "parentFirstName"
from "Person"
inner join "Person" as "Parent" on "Person"."parentId" = "Parent"."id"
where "Person"."age" < (select avg("Person"."age") from "Person")
and exists (select 1 from "Animal" where "Person"."id" = "Animal"."ownerId")
order by "Person"."lastName" asc

Update models:

Person
  .query()
  .patch({lastName: 'Dinosaur'})
  .where('age', '>', 60)
  .then(function (numUpdated) {
    console.log('all persons over 60 years old are now dinosaurs');
    console.log(numUpdated, 'people were updated');.
  })
  .catch(function (err) {
    console.log(err.stack);
  });
update "Person" set "lastName" = 'Dinosaur' where "age" > 60

The .patch() and .update() method return the number of updated rows. If you want the fresly updated model as aresult you can use the helper method .patchAndFetchById() and .updateAndFetchById().

Person
  .query()
  .patchAndFetchById(246, {lastName: 'Updated'})
  .then(function (updated) {
    console.log(updated.lastName); // --> Updated.
  })
  .catch(function (err) {
    console.log(err.stack);
  });
update "Person" set "lastName" = 'Updated' where "id" = 246
select * from "Person" where "id" = 246

While the static .query() method can be used to create a query to a whole table .$relatedQuery() method can be used to query a single relation. .$relatedQuery() returns an instance of QueryBuilder just like the .query() method.

var jennifer;
Person
  .query()
  .where('firstName', 'Jennifer')
  .first()
  .then(function (person) {
    jennifer = person;
    return jennifer
      .$relatedQuery('pets')
      .where('species', 'dog')
      .orderBy('name');
  })
  .then(function (jennifersDogs) {
    console.log(jennifersDogs[0] instanceof Animal); // --> true
    console.log(jennifer.pets === jennifersDogs); // --> true
    console.log('Jennifer has', jennifersDogs.length, 'dogs');
  })
  .catch(function (err) {
    console.log(err.stack);
  });
select * from "Person" where "firstName" = 'Jennifer'

select * from "Animal"
where "species" = 'dog'
and "Animal"."ownerId" = 1
order by "name" asc

Insert a related model:

Person
  .query()
  .where('id', 1)
  .first()
  .then(function (person) {
    return person.$relatedQuery('pets').insert({name: 'Fluffy'});
  })
  .then(function (fluffy) {
    console.log(fluffy.id);
  })
  .catch(function (err) {
    console.log('something went wrong with finding the person OR inserting the pet');
    console.log(err.stack);
  });
select * from "Person" where "id" = 1

insert into "Animal" ("name", "ownerId") values ('Fluffy', 1)

Eager queries

Okay I said there is no custom DSL but actually we have teeny-tiny one for fetching relations eagerly, as it isn't something that can be done easily using SQL. In addition to making your life easier, eager queries avoid the select N+1 problem and provide a great performance. The following examples demonstrate how to use it:

Fetch the pets relation for all results of a query:

Person
  .query()
  .eager('pets')
  .then(function (persons) {
    // Each person has the `.pets` property populated with Animal objects related
    // through `pets` relation.
    console.log(persons[0].pets[0].name);
    console.log(persons[0].pets[0] instanceof Animal); // --> true
  });

Fetch multiple relations on multiple levels:

Person
  .query()
  .eager('[pets, children.[pets, children]]')
  .then(function (persons) {
    // Each person has the `.pets` property populated with Animal objects related
    // through `pets` relation. The `.children` property contains the Person's
    // children. Each child also has the `pets` and `children` relations eagerly
    // fetched.
    console.log(persons[0].pets[0].name);
    console.log(persons[1].children[2].pets[1].name);
    console.log(persons[1].children[2].children[0].name);
  });

Fetch one relation recursively:

Person
  .query()
  .eager('[pets, children.^]')
  .then(function (persons) {
    // The children relation is from Person to Person. If we want to fetch the whole
    // descendant tree of a person we can just say "fetch this relation recursively"
    // using the `.^` notation.
    console.log(persons[0].children[0].children[0].children[0].children[0].firstName);
  });

Relations can be filtered using named filters like this:

Person
  .query()
  .eager('[pets(orderByName, onlyDogs), children(orderByAge).[pets, children]]', {
    orderByName: function (builder) {
      builder.orderBy('name');
    },
    orderByAge: function (builder) {
      builder.orderBy('age')
    },
    onlyDogs: function (builder) {
      builder.where('species', 'dog')
    }
  })
  .then(function (persons) {
    console.log(persons[0].children[0].pets[0].name);
    console.log(persons[0].children[0].movies[0].id); 
  });

The expressions can be arbitrarily deep. See the full description here.

Because the eager expressions are strings they can be easily passed for example as a query parameter of an HTTP request. However, allowing the client to pass expressions like this without any limitations is not very secure. Therefore the QueryBuilder has the .allowEager method. allowEager can be used to limit the allowed eager expression to a certain subset. Like this:

expressApp.get('/persons', function (req, res, next) {
  Person
    .query()
    .allowEager('[pets, children.pets]')
    .eager(req.query.eager)
    .then(function (persons) { res.send(persons); })
    .catch(next);
});

The example above allows req.query.eager to be one of:

  • 'pets'
  • 'children'
  • 'children.pets'
  • '[pets, children]'
  • '[pets, children.pets]'.

Examples of failing eager expressions are:

  • 'movies'
  • 'children.children'
  • '[pets, children.children]'
  • 'notEvenAnExistingRelation'.

In addition to the .eager method, relations can be fetched using the loadRelated and $loadRelated methods of Model.

Eager inserts

Arbitrary relation trees can be inserted using the insertWithRelated() method:

Person
  .query()
  .insertWithRelated({
    firstName: 'Sylvester',
    lastName: 'Stallone',

    children: [{
      firstName: 'Sage',
      lastName: 'Stallone',

      pets: [{
        name: 'Fluffy',
        species: 'dog'
      }]
    }]
  });

The query above will insert 'Sylvester', 'Sage' and 'Fluffy' into db and create relationships between them as defined in the relationMappings of the models. Technically insertWithRelated builds a dependency graph from the object tree and inserts the models that don't depend on any other models until the whole tree is inserted.

If you need to refer to the same model in multiple places you can use the special properties #id and #ref like this:

Person
  .query()
  .insertWithRelated([{
    firstName: 'Jennifer',
    lastName: 'Lawrence',

    movies: [{
      "#id": 'silverLiningsPlaybook'
      name: 'Silver Linings Playbook',
      duration: 122
    }]
  }, {
    firstName: 'Bradley',
    lastName: 'Cooper',

    movies: [{
      "#ref": 'silverLiningsPlaybook'
    }]
  }]);

The query above will insert only one movie (the 'Silver Linings Playbook') but both 'Jennifer' and 'Bradley' will have the movie related to them through the many-to-many relation movies. The #id can be any string. There are no format or length requirements for them. It is quite easy to create circular dependencies using #id and #ref. Luckily insertWithRelated detects them and rejects the query with a clear error message.

You can refer to the properties of other models anywhere in the graph using expressions of format #ref{<id>.<property>} as long as the reference doesn't create a circular dependency. For example:

Person
  .query()
  .insertWithRelated([{
    "#id": 'jenniLaw',
    firstName: 'Jennifer',
    lastName: 'Lawrence',

    pets: [{
      name: "I am the dog of #ref{jenniLaw.firstName} whose id is #ref{jenniLaw.id}",
      species: 'dog'
    }]
  }]);

The query above will insert a pet named I am the dog of Jennifer whose id is 523 for Jennifer. If #ref{} is used within a string, the references are replaced with the referred values inside the string. If the reference string contains nothing but the reference, the referred value is copied to it's place preserving its type.

See the allowInsert method if you need to limit which relations can be inserted using insertWithRelated method to avoid security issues.

By the way, if you are using Postgres the inserts are done in batches for maximum performance.

Transactions

There are two ways to use transactions in objection.js

  1. Transaction callback
  2. Transaction object

Transaction callback

The first way to work with transactions is to perform all operations inside one callback using the objection.transaction function. The beauty of this method is that you don't need to pass a transaction object to each query explicitly as long as you start all queries using the bound model classes that are passed to the transaction callback as arguments.

Transactions are started by calling the objection.transaction function. Give all the models you want to use in the transaction as parameters to the transaction function. The model classes are bound to a newly created transaction and passed to the callback function. Inside this callback, all queries started through them take part in the same transaction.

The transaction is committed if the returned Promise is resolved successfully. If the returned Promise is rejected the transaction is rolled back.

objection.transaction(Person, Animal, function (Person, Animal) {

  return Person
    .query()
    .insert({firstName: 'Jennifer', lastName: 'Lawrence'})
    .then(function () {
      return Animal
        .query()
        .insert({name: 'Scrappy'});
    });

}).then(function (scrappy) {
  console.log('Jennifer and Scrappy were successfully inserted');
}).catch(function (err) {
  console.log('Something went wrong. Neither Jennifer nor Scrappy were inserted');
});

You only need to give the transaction function the model classes you use explicitly. All the related model classes are implicitly bound to the same transaction.

objection.transaction(Person, function (Person) {

  return Person
    .query()
    .insert({firstName: 'Jennifer', lastName: 'Lawrence'})
    .then(function (jennifer) {
      // This creates a query using the `Animal` model class but we
      // don't need to give `Animal` as one of the arguments to the
      // transaction function.
      return jennifer
        .$relatedQuery('pets')
        .insert({name: 'Scrappy'});
    });

}).then(function (scrappy) {
  console.log('Jennifer and Scrappy were successfully inserted');
}).catch(function (err) {
  console.log('Something went wrong. Neither Jennifer nor Scrappy were inserted');
});

The only way you can mess up with the transactions is if you explicitly start a query using a model class that is not bound to the transaction:

var Person = require('./models/Person');
var Animal = require('./models/Animal');

objection.transaction(Person, function (Person) {

  return Person
    .query()
    .insert({firstName: 'Jennifer', lastName: 'Lawrence'})
    .then(function (jennifer) {
      // OH NO! This query is executed outside the transaction
      // since the `Animal` class is not bound to the transaction.
      return Animal
        .query()
        .insert({name: 'Scrappy'});
    });

});

Transaction object

The second way to use transactions is to express the transaction as an object and bind model classes to the transaction when you use them. This way is more convenient when you need to pass the transaction to functions and services.

The transaction object can be created using the objection.transaction.start method. You need to remember to call either the commit or rollback method of the transaction object.

// You need to pass some model (any model with a knex connection)
// or the knex connection itself to the start method.
var trx;
objection.transaction.start(Person).then(function (transaction) {
  trx = transaction;
  return Person
    .bindTransaction(trx)
    .query()
    .insert({firstName: 'Jennifer', lastName: 'Lawrence'});
}).then(function (jennifer) {
  // jennifer was created using a bound model class. Therefore
  // all queries started through it automatically take part in
  // the same transaction. We don't need to bind anything here.
  return jennifer
    .$relatedQuery('pets')
    .insert({name: 'Fluffy'});
}).then(function () {
  return Movie
    .bindTransaction(trx)
    .query()
    .where('name', 'ilike', '%forrest%');
}).then(function (movies) {
  console.log(movies);
  return trx.commit();
}).catch(function () {
  return trx.rollback();
});

To understand what is happening in the above example you need to know how bindTransaction method works. bindTransaction actually returns an anonymous subclass of the model class you call it for and sets the transaction's database connection as that class's database connection. This way all queries started through that anonymous class use the same connection and take part in the same transaction. If we didn't create a subclass and just set the original model's database connection, all query chains running in parallel would suddenly jump into the same transaction and things would go terribly wrong.

Now that you have an idea how the bindTransaction works you should see that the previous example could also be implemented like this:

var BoundPerson;
var BoundMovie;

objection.transaction.start(Person).then(function (transaction) {
  BoundPerson = Person.bindTransaction(transaction);
  BoundMovie = Movie.bindTransaction(transaction);
  
  return BoundPerson
    .query()
    .insert({firstName: 'Jennifer', lastName: 'Lawrence'});
}).then(function (jennifer) {
  return jennifer
    .$relatedQuery('pets')
    .insert({name: 'Fluffy'});
}).then(function () {
  return BoundMovie
    .query()
    .where('name', 'ilike', '%forrest%');
}).then(function (movies) {
  console.log(movies);
  return BoundPerson.knex().commit();
}).catch(function () {
  return BoundPerson.knex().rollback();
});

which is pretty much what the objection.transaction function does.

Documents

Objection.js makes it easy to store non-flat documents as table rows. All properties of a model that are marked as objects or arrays in the model's jsonSchema are automatically converted to JSON strings in the database and back to objects when read from the database. The database columns for the object properties can be normal text columns. Postgresql has the json and jsonb data types that can be used instead for better performance and possibility to make queries to the documents.

The address property of the Person model is defined as an object in the Person.jsonSchema:

Person
  .query()
  .insert({
    firstName: 'Jennifer',
    lastName: 'Lawrence',
    age: 24,
    address: {
      street: 'Somestreet 10',
      zipCode: '123456',
      city: 'Tampere'
    }
  })
  .then(function (jennifer) {
    console.log(jennifer.address.city); // --> Tampere
    return Person.query().where('id', jennifer.id);
  })
  .then(function (jenniferFromDb) {
    console.log(jenniferFromDb.address.city); // --> Tampere
  })
  .catch(function (err) {
    console.log('oh noes');
  });

Validation

JSON schema validation can be enabled by setting the jsonSchema property of a model class. The validation is ran each time a Model instance is created. For example all these will trigger the validation:

Person.fromJson({firstName: 'jennifer', lastName: 'Lawrence'});
Person.query().insert({firstName: 'jennifer', lastName: 'Lawrence'});
Person.query().update({firstName: 'jennifer', lastName: 'Lawrence'}).where('id', 10);
// Patch operation ignores the `required` property of the schema and only validates the
// given properties. This allows a subset of model's properties to be updated.
Person.query().patch({age: 24}).where('age', '<', 24);

You rarely need to call $validate method explicitly, but you can do it when needed. If validation fails a ValidationError will be thrown. Since we use Promises, this usually means that a promise will be rejected with an instance of ValidationError.

Person.query().insert({firstName: 'jennifer'}).catch(function (err) {
  console.log(err instanceof objection.ValidationError); // --> true
  console.log(err.data); // --> {lastName: 'required property missing'}
});

See the recipe book for instructions if you want to use some other validation library.

Models

Models are created by inheriting from the Model base class. In objection.js the inheritance is done as transparently as possible. There is no custom Class abstraction making you wonder what the hell is happening. Just plain old ugly javascript inheritance.

Minimal model

A working model with minimal amount of code:

var Model = require('objection').Model;

function MinimalModel() {
  Model.apply(this, arguments);
}

// Inherit `Model`. This does the basic prototype inheritance but also 
// inherits all the static methods and properties like `Model.query()` 
// and `Model.fromJson()`. This is consistent with ES6 class inheritance.
Model.extend(MinimalModel);

// After the js class boilerplate, all you need to do is set the table name.
MinimalModel.tableName = 'SomeTableName';

module.exports = MinimalModel;

A model with custom methods, json schema validation and relations

This is the model used in the examples:

var Model = require('objection').Model;

function Person() {
  Model.apply(this, arguments);
}

Model.extend(Person);
module.exports = Person;

// You can add custom functionality to Models just as you would
// to any javascript class.
Person.prototype.fullName = function () {
  return this.firstName + ' ' + this.lastName;
};

// Table name is the only required property.
Person.tableName = 'Person';

// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
Person.jsonSchema = {
  type: 'object',
  required: ['firstName', 'lastName'],

  properties: {
    id: {type: 'integer'},
    parentId: {type: ['integer', 'null']},
    firstName: {type: 'string', minLength: 1, maxLength: 255},
    lastName: {type: 'string', minLength: 1, maxLength: 255},
    age: {type: 'number'},

    address: {
      type: 'object',
      properties: {
        street: {type: 'string'},
        city: {type: 'string'},
        zipCode: {type: 'string'}
      }
    }
  }
};

// This object defines the relations to other models.
Person.relationMappings = {
  pets: {
    relation: Model.OneToManyRelation,
    // The related model. This can be either a Model subclass constructor or an
    // absolute file path to a module that exports one. We use the file path version
    // here to prevent require loops.
    modelClass: __dirname + '/Animal',
    join: {
      from: 'Person.id',
      to: 'Animal.ownerId'
    }
  },

  movies: {
    relation: Model.ManyToManyRelation,
    modelClass: __dirname + '/Movie',
    join: {
      from: 'Person.id',
      // ManyToMany relation needs the `through` object to describe the join table.
      through: {
        from: 'Person_Movie.personId',
        to: 'Person_Movie.movieId'
      },
      to: 'Movie.id'
    }
  },

  parent: {
    relation: Model.OneToOneRelation,
    modelClass: Person,
    join: {
      from: 'Person.parentId',
      to: 'Person.id'
    }
  },

  children: {
    relation: Model.OneToManyRelation,
    modelClass: Person,
    join: {
      from: 'Person.id',
      to: 'Person.parentId'
    }
  }
};

Testing

To run the tests, all you need to do is configure the databases and run npm test. Check out this file for the test database configurations. If you don't want to run the tests against all databases you can just comment out configurations from the testDatabaseConfigs list.

Changelog

0.3.0

What's new

Breaking changes

  • QueryBuilder methods update, patch and delete now return the number of affected rows. The new methods updateAndFetchById and patchAndFetchById may help with the migration
  • modelInstance.$query() instance method now returns a single model instead of an array
  • Removed Model.generateId() method. $beforeInsert can be used instead

0.2.8

What's new

  • ES6 inheritance support
  • generator function support for transactions
  • traverse,pick and omit methods for Model and QueryBuilder
  • bugfix: issue #38

0.2.7

What's new

  • bugfix: fix #37 also for $query().
  • Significant toJson/fromJson performance boost.

0.2.6

What's new

  • bugfix: fix regression bug that broke dumpSql.

0.2.5

What's new

  • bugfix: fix regression bug that prevented values assigned to this in $before callbacks from getting into the actual database query

0.2.4

What's new

  • bugfix: many-to-many relations didn't work correctly with a snake_case to camelCase conversion in the related model class.

0.2.3

What's new

  • Promise constructor is now exposed through require('objection').Promise.

0.2.2

What's new

  • $beforeUpdate, $afterUpdate, $beforeInsert etc. are now asynchronous and you can return promises from them.
  • Added Model.fn() shortcut to knex.fn.
  • Added missing asCallback and nodeify methods for QueryBuilder.

0.2.1

What's new

  • bugfix: Chaining insert with returning now returns all listed columns.

0.2.0

What's new

  • New name objection.js.
  • $beforeInsert, $afterInsert, $beforeUpdate and $afterUpdate hooks for Model.
  • Postgres jsonb query methods: whereJsonEquals, whereJsonSupersetOf, whereJsonSubsetOf and friends.
  • whereRef query method.
  • Expose knex.raw() through Model.raw().
  • Expose knex.client.formatter() through Model.formatter().
  • QueryBuilder can be used to make sub queries just like knex's QueryBuilder.
  • Possibility to use a custom QueryBuilder subclass by overriding Model.QueryBuilder.
  • Filter queries/objects for relations.
  • A pile of bug fixes.

Breaking changes

  • Project was renamed to objection.js. Migrate simply by replacing moron with objection.

0.1.0

First release.

About

An SQL-friendly ORM for Node.js

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 81.1%
  • CSS 18.9%