diff --git a/doc/includes/API.md b/doc/includes/API.md
index 084b5a3e2..2c2389fae 100644
--- a/doc/includes/API.md
+++ b/doc/includes/API.md
@@ -1,5 +1,51 @@
# API reference
+## The main module
+
+```js
+const mainModule = require('objection');
+const { Model, ref } = require('objection');
+```
+
+The main module is what you get when you import objection. It has a bunch of fields that are all
+documented elsewhere in the API docs. Here's a list of the fields and links to their docs.
+
+### Fields
+
+
Model
+
+[The model base class.](#model)
+
+transaction
+
+[The transaction function.](#transactions)
+
+ref
+
+[The ref helper function.](#ref)
+
+raw
+
+[The raw helper function.](#raw)
+
+mixin
+
+[The mixin helper](#plugins) for applying plugins. See the examples behind this link.
+
+compose
+
+[The compose helper](#plugins) for applying plugins. See the examples behind this link.
+
+lodash
+
+[Lodash utility library](https://lodash.com/) used in objection. Useful for plugin developers so that
+they don't have to add it as a dependency.
+
+Promise
+
+[Bluebird promise library](http://bluebirdjs.com/docs/getting-started.html) used in objection. Useful for plugin developers so that
+they don't have to add it as a dependency.
+
## QueryBuilder
Query builder for Models.
diff --git a/doc/includes/RECIPES.md b/doc/includes/RECIPES.md
index fcfbaed5a..cc260d86f 100644
--- a/doc/includes/RECIPES.md
+++ b/doc/includes/RECIPES.md
@@ -216,7 +216,7 @@ class Person extends Model {
// This is called when an object is read from database.
$parseDatabaseJson(json) {
- json = _.mapKeys(json, function (value, key) {
+ json = _.mapKeys(json, (value, key) => {
return camelCase(key);
});
@@ -711,9 +711,9 @@ Complete example how to try out different index choices.
> Migration:
```js
-exports.up = function (knex) {
+exports.up = (knex) => {
return knex.schema
- .createTable('Hero', function (table) {
+ .createTable('Hero', (table) => {
table.increments('id').primary();
table.string('name');
table.jsonb('details');
@@ -728,7 +728,7 @@ exports.up = function (knex) {
"CREATE INDEX on ?? ((??#>>'{type}'))",
['Hero', 'details']
)
- .createTable('Place', function (table) {
+ .createTable('Place', (table) => {
table.increments('id').primary();
table.string('name');
table.jsonb('details');
diff --git a/doc/index.md b/doc/index.md
index b4631f271..9178f72f4 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -1601,9 +1601,10 @@ create a pull request or an issue to get it added to this list.
```js
function SomeMixin(Model) {
- return SomeExtendedModel extends Model {
+ // The returned class should have no name.
+ return class extends Model {
// Your modifications.
- }
+ };
}
```
@@ -1623,6 +1624,48 @@ class Person extends SomeMixin(SomeOtherMixin(Model)) {
}
```
+> There are a couple of helpers in objection main module for applying multiple mixins.
+
+```js
+const { mixin, Model } = require('objection');
+
+class Person extends mixin(Model, [
+ SomeMixin,
+ SomeOtherMixin,
+ EvenMoreMixins,
+ LolSoManyMixins,
+ ImAMixinWithOptions({foo: 'bar'})
+]) {
+
+}
+```
+
+```js
+const { compose, Model } = require('objection');
+
+const mixins = compose(
+ SomeMixin,
+ SomeOtherMixin,
+ EvenMoreMixins,
+ LolSoManyMixins,
+ ImAMixinWithOptions({foo: 'bar'})
+);
+
+class Person extends mixins(Model) {
+
+}
+```
+
+> Mixins can also be used as decorators:
+
+```js
+@SomeMixin
+@MixinWithOptions({foo: 'bar'})
+class Person extends Model {
+
+}
+```
+
When possible, objection.js plugins should be implemented as [class mixins](http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/).
A mixin is simply a function that takes a class as an argument and returns a subclass. Plugins should
avoid modifying `objection.Model`, `objection.QueryBuilder` or any other global variables directly.
diff --git a/examples/plugin-with-options/index.js b/examples/plugin-with-options/index.js
index 21bc01801..b7c3ff6c5 100644
--- a/examples/plugin-with-options/index.js
+++ b/examples/plugin-with-options/index.js
@@ -39,7 +39,11 @@ module.exports = (options) => {
}
}
- class SessionModel extends Model {
+ // A Plugin always needs to return the extended model class.
+ //
+ // IMPORTANT: Don't give a name for the returned class! This way the returned
+ // class inherits the super class's name (starting from node 8).
+ return class extends Model {
// Make our model use the extended QueryBuilder.
static get QueryBuilder() {
@@ -83,9 +87,6 @@ module.exports = (options) => {
}
});
}
- }
-
- // A Plugin always needs to return the extended model class.
- return SessionModel;
+ };
};
};
\ No newline at end of file
diff --git a/examples/plugin/index.js b/examples/plugin/index.js
index da379ff8e..2839cf0a6 100644
--- a/examples/plugin/index.js
+++ b/examples/plugin/index.js
@@ -24,7 +24,11 @@ module.exports = (Model) => {
}
}
- class SessionModel extends Model {
+ // A Plugin always needs to return the extended model class.
+ //
+ // IMPORTANT: Don't give a name for the returned class! This way the returned
+ // class inherits the super class's name (starting from node 8).
+ return class extends Model {
// Make our model use the extended QueryBuilder.
static get QueryBuilder() {
@@ -58,8 +62,5 @@ module.exports = (Model) => {
}
});
}
- }
-
- // A Plugin always needs to return the extended model class.
- return SessionModel;
+ };
};
\ No newline at end of file
diff --git a/lib/objection.js b/lib/objection.js
index 0ea83ef97..981544fbf 100644
--- a/lib/objection.js
+++ b/lib/objection.js
@@ -9,6 +9,8 @@ const ValidationError = require('./model/ValidationError');
const NotFoundError = require('./model/NotFoundError');
const AjvValidator = require('./model/AjvValidator');
const Validator = require('./model/Validator');
+const compose = require('./utils/mixin').compose;
+const mixin = require('./utils/mixin').mixin;
const Relation = require('./relations/Relation');
const HasOneRelation = require('./relations/hasOne/HasOneRelation');
@@ -20,6 +22,8 @@ const ManyToManyRelation = require('./relations/manyToMany/ManyToManyRelation');
const transaction = require('./transaction');
const ref = require('./queryBuilder/ReferenceBuilder').ref;
const raw = require('./queryBuilder/RawBuilder').raw;
+
+const lodash = require('lodash');
const Promise = require('bluebird');
module.exports = {
@@ -39,8 +43,12 @@ module.exports = {
HasOneThroughRelation,
ManyToManyRelation,
transaction,
- Promise,
+ compose,
+ mixin,
ref,
- raw
+ raw,
+
+ Promise,
+ lodash
};
diff --git a/lib/utils/mixin.js b/lib/utils/mixin.js
new file mode 100644
index 000000000..12f5ba312
--- /dev/null
+++ b/lib/utils/mixin.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const flatten = require('lodash/flatten');
+const toArray = require('lodash/toArray');
+const tail = require('lodash/tail');
+
+function mixin() {
+ const args = flatten(arguments);
+ const mixins = tail(args);
+
+ return mixins.reduce((Class, mixinFunc) => {
+ return mixinFunc(Class);
+ }, args[0]);
+}
+
+function compose() {
+ const mixins = flatten(arguments);
+
+ return function (Class) {
+ return mixin(Class, mixins);
+ };
+}
+
+module.exports = {
+ compose,
+ mixin
+};
\ No newline at end of file
diff --git a/tests/integration/insert.js b/tests/integration/insert.js
index 96f97c174..6bb0f09ec 100644
--- a/tests/integration/insert.js
+++ b/tests/integration/insert.js
@@ -1147,6 +1147,7 @@ module.exports = (session) => {
expect(_.filter(rows, {model1Id: inserted.id, model2Id: parent.idCol})).to.have.length(1);
});
});
+
});
});
diff --git a/tests/integration/update.js b/tests/integration/update.js
index dcca31afe..918a59f7e 100644
--- a/tests/integration/update.js
+++ b/tests/integration/update.js
@@ -315,7 +315,7 @@ module.exports = (session) => {
});
it('should update a model (1)', () => {
- let model = Model1.fromJson({id: 1});
+ const model = Model1.fromJson({id: 1});
return model
.$query()
@@ -333,7 +333,7 @@ module.exports = (session) => {
});
it('should update a model (2)', () => {
- let model = Model1.fromJson({id: 1, model1Prop1: 'updated text'});
+ const model = Model1.fromJson({id: 1, model1Prop1: 'updated text'});
return model
.$query()
diff --git a/tests/main.js b/tests/main.js
index 2b326923b..77d765bea 100644
--- a/tests/main.js
+++ b/tests/main.js
@@ -26,6 +26,9 @@ describe('main module', () => {
expect(objection.Promise).to.equal(require('bluebird'));
expect(objection.Validator).to.equal(require('../lib/model/Validator'));
expect(objection.AjvValidator).to.equal(require('../lib/model/AjvValidator'));
+ expect(objection.mixin).to.equal(require('../lib/utils/mixin').mixin);
+ expect(objection.compose).to.equal(require('../lib/utils/mixin').compose);
+ expect(objection.lodash).to.equal(require('lodash'));
});
});
diff --git a/tests/unit/utils.js b/tests/unit/utils.js
index cf7fe5e78..5d868bc50 100644
--- a/tests/unit/utils.js
+++ b/tests/unit/utils.js
@@ -3,6 +3,8 @@
const util = require('util');
const expect = require('expect.js');
const utils = require('../../lib/utils/classUtils');
+const compose = require('../../lib/utils/mixin').compose;
+const mixin = require('../../lib/utils/mixin').mixin;
describe('utils', () => {
@@ -31,4 +33,67 @@ describe('utils', () => {
});
+ describe('mixin', () => {
+
+ it('should mixin rest of the arguments to the first argument', () => {
+ class X {}
+
+ const m1 = C => class extends C {
+ f() {
+ return 1;
+ }
+ };
+
+ const m2 = C => class extends C {
+ f() {
+ return super.f() + 1;
+ }
+ };
+
+ const Y = mixin(X, m1, m2);
+ const y = new Y();
+
+ expect(y.f()).to.equal(2);
+
+ const Z = mixin(X, [m1, m2]);
+ const z = new Z();
+
+ expect(z.f()).to.equal(2);
+ });
+
+ });
+
+ describe('compose', () => {
+
+ it('should compose multiple functions', () => {
+ class X {}
+
+ const m1 = C => class extends C {
+ f() {
+ return 1;
+ }
+ };
+
+ const m2 = C => class extends C {
+ f() {
+ return super.f() + 1;
+ }
+ };
+
+ const m3 = compose(m1, m2);
+ const m4 = compose([m1, m2]);
+
+ const Y = m3(X);
+ const y = new Y();
+
+ expect(y.f()).to.equal(2);
+
+ const Z = m4(X);
+ const z = new Z();
+
+ expect(z.f()).to.equal(2);
+ });
+
+ });
+
});