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); + }); + + }); + });