Skip to content

Commit

Permalink
mixin and compose plugin helpers. closes #475, #473
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Aug 10, 2017
1 parent 4ff3a92 commit db546d4
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 20 deletions.
46 changes: 46 additions & 0 deletions doc/includes/API.md
Original file line number Diff line number Diff line change
@@ -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

<h4 id="objection-model">Model</h4>

[The model base class.](#model)

<h4 id="objection-transaction">transaction</h4>

[The transaction function.](#transactions)

<h4 id="objection-ref">ref</h4>

[The ref helper function.](#ref)

<h4 id="objection-raw">raw</h4>

[The raw helper function.](#raw)

<h4 id="objection-mixin">mixin</h4>

[The mixin helper](#plugins) for applying plugins. See the examples behind this link.

<h4 id="objection-compose">compose</h4>

[The compose helper](#plugins) for applying plugins. See the examples behind this link.

<h4 id="objection-lodash">lodash</h4>

[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.

<h4 id="objection-promise">Promise</h4>

[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.
Expand Down
8 changes: 4 additions & 4 deletions doc/includes/RECIPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand Down
47 changes: 45 additions & 2 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
};
}
```

Expand All @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions examples/plugin-with-options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -83,9 +87,6 @@ module.exports = (options) => {
}
});
}
}

// A Plugin always needs to return the extended model class.
return SessionModel;
};
};
};
11 changes: 6 additions & 5 deletions examples/plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -58,8 +62,5 @@ module.exports = (Model) => {
}
});
}
}

// A Plugin always needs to return the extended model class.
return SessionModel;
};
};
12 changes: 10 additions & 2 deletions lib/objection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 = {
Expand All @@ -39,8 +43,12 @@ module.exports = {
HasOneThroughRelation,
ManyToManyRelation,
transaction,
Promise,
compose,
mixin,
ref,
raw
raw,

Promise,
lodash
};

27 changes: 27 additions & 0 deletions lib/utils/mixin.js
Original file line number Diff line number Diff line change
@@ -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
};
1 change: 1 addition & 0 deletions tests/integration/insert.js
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@ module.exports = (session) => {
expect(_.filter(rows, {model1Id: inserted.id, model2Id: parent.idCol})).to.have.length(1);
});
});

});

});
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});

});
65 changes: 65 additions & 0 deletions tests/unit/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand Down Expand Up @@ -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);
});

});

});

0 comments on commit db546d4

Please sign in to comment.