Skip to content

Commit fc2a805

Browse files
committed
feat(relations): adds hasMany and hasOne hooks.
Also adds hasMany `params` option. Closes #35, #28
1 parent db47f98 commit fc2a805

File tree

4 files changed

+195
-18
lines changed

4 files changed

+195
-18
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,8 @@ Will send GET to /bikes/1/parts/:id instead of /parts/:id
488488

489489
<!-- end -->
490490

491+
You can also define `hasMany` relation hooks. Check the [hooks advanced documentation](https://github.com/platanus/angular-restmod/blob/master/docs/guides/hooks.md) for more information.
492+
491493
<!-- end -->
492494

493495
#### HasOne
@@ -579,6 +581,8 @@ owner.$save();
579581
580582
<!-- end -->
581583

584+
You can also define `hasOne` relation hooks. Check the [hooks advanced documentation](https://github.com/platanus/angular-restmod/blob/master/docs/guides/hooks.md) for more information.
585+
582586
<!-- ignore -->
583587

584588
#### BelongsTo

docs/guides/hooks.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,63 @@ bike = bikes.$new();
4747
bike.$save(); // this call will trigger the hook defined above
4848
```
4949

50-
##### 4. Hooks that apply only to a certain execution context using `$decorate`, used mainly to modify a method behaviour
50+
##### 4. Hooks that apply to a relation object
51+
52+
The `hasMany` and `hasOne` relations support hooks, these can be defined in the relation definition or in the type config block
53+
54+
In the relation definition:
55+
56+
```javascript
57+
var Bike = $restmod.model('/bikes'.$mix({
58+
parts: {
59+
hasMany: 'Part',
60+
hooks: {
61+
'after-fetch-many': function() { /* do something with the new parts */ }
62+
}
63+
}
64+
});
65+
```
66+
67+
or in the configuration block (applies to every relation where the extended class is the **NESTED** resource):
68+
69+
```javascript
70+
Var Part = $restmod.model().$mix({
71+
$config {
72+
hasMany: {
73+
hooks: {
74+
'after-has-many-init': function() {
75+
// Here we are using the init hook to override every hasMany Part relation scope.
76+
// This can be useful if a custom url scheme is needed for relations.
77+
this.$scope = customScope(this.$scope);
78+
}
79+
}
80+
}
81+
}
82+
})
83+
```
84+
85+
To access the owner object inside a relation hook, use the child object's `$owner` property:
86+
87+
```javascript
88+
var Bike = $restmod.model('/bikes').$mix({
89+
parts: {
90+
hasMany: 'Part',
91+
hooks: {
92+
'after-fetch-many': function() {
93+
this.$owner.partsCount = this.length; // update the owner partCount every time the relation is fetched!
94+
}
95+
}
96+
}
97+
});
98+
```
99+
100+
Both `hasOne` and `hasMany` trigger an special event after hooks are added to the child resource, this enables you to specify some
101+
logic to be run after relation resource is initialized (since hooks are added AFTER `after-init` and `after-collection-init` are triggered).
102+
* `hasMany` triggers `after-has-many-init` after `after-collection-init`.
103+
* `hasOne` triggers `after-has-one-init` after `after-init`.
104+
105+
106+
##### 5. Hooks that apply only to a certain execution context using `$decorate`, used mainly to modify a method behaviour
51107
52108
```javascript
53109
var Bike = $restmod.model('/bikes', {

src/module/extended/builder-relations.js

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22

33
RMModule.factory('RMBuilderRelations', ['$injector', 'inflector', '$log', 'RMUtils', 'restmod', 'RMPackerCache', function($injector, inflector, $log, Utils, restmod, packerCache) {
44

5+
// wraps a hook callback to give access to the $owner object
6+
function wrapHook(_fun, _owner) {
7+
return function() {
8+
var oldOwner = this.$owner;
9+
this.$owner = _owner;
10+
try {
11+
return _fun.apply(this, arguments);
12+
} finally {
13+
this.$owner = oldOwner;
14+
}
15+
};
16+
}
17+
18+
// wraps a bunch of hooks
19+
function applyHooks(_target, _hooks, _owner) {
20+
for(var key in _hooks) {
21+
if(_hooks.hasOwnProperty(key)) {
22+
_target.$on(key, wrapHook(_hooks[key], _owner));
23+
}
24+
}
25+
}
26+
527
/**
628
* @class RelationBuilderApi
729
*
@@ -27,15 +49,23 @@ RMModule.factory('RMBuilderRelations', ['$injector', 'inflector', '$log', 'RMUti
2749
* @param {string} _url Partial url
2850
* @param {string} _source Inline resource alias (optional)
2951
* @param {string} _inverseOf Inverse property name (optional)
52+
* @param {object} _params Generated collection default parameters
53+
* @param {object} _hooks Hooks to be applied just to the generated collection
3054
* @return {BuilderApi} self
3155
*/
32-
attrAsCollection: function(_attr, _model, _url, _source, _inverseOf) {
56+
attrAsCollection: function(_attr, _model, _url, _source, _inverseOf, _params, _hooks) {
57+
58+
var options, globalHooks; // global relation configuration
3359

3460
this.attrDefault(_attr, function() {
3561

3662
if(typeof _model === 'string') {
3763
_model = $injector.get(_model);
3864

65+
// retrieve global options
66+
options = _model.getProperty('hasMany', {});
67+
globalHooks = options.hooks;
68+
3969
if(_inverseOf) {
4070
var desc = _model.$$getDescription(_inverseOf);
4171
if(!desc || desc.relation !== 'belongs_to') {
@@ -45,25 +75,23 @@ RMModule.factory('RMBuilderRelations', ['$injector', 'inflector', '$log', 'RMUti
4575
}
4676
}
4777

48-
var self = this,
49-
scope = this.$buildScope(_model, _url || inflector.parameterize(_attr)),
50-
col = _model.$collection(null, scope);
78+
var scope = this.$buildScope(_model, _url || inflector.parameterize(_attr)), col; // TODO: name to url transformation should be a Model strategy
5179

52-
// TODO: there should be a way to modify scope behavior just for this relation,
53-
// since relation item scope IS the collection, then the collection should
54-
// be extended to provide a modified scope. For this an additional _extensions
55-
// parameters could be added to collection, then these 'extensions' are inherited
56-
// by child collections, the other alternative is to enable full property inheritance ...
80+
// setup collection
81+
col = _model.$collection(_params || null, scope);
82+
if(globalHooks) applyHooks(col, globalHooks, this);
83+
if(_hooks) applyHooks(col, _hooks, this);
84+
col.$dispatch('after-has-many-init');
5785

5886
// set inverse property if required.
5987
if(_inverseOf) {
88+
var self = this;
6089
col.$on('after-add', function(_obj) {
6190
_obj[_inverseOf] = self;
6291
});
6392
}
6493

6594
return col;
66-
// simple support for inline data, TODO: maybe deprecate this.
6795
});
6896

6997
if(_source || _url) this.attrMap(_attr, _source || _url);
@@ -87,15 +115,22 @@ RMModule.factory('RMBuilderRelations', ['$injector', 'inflector', '$log', 'RMUti
87115
* @param {string} _url Partial url (optional)
88116
* @param {string} _source Inline resource alias (optional)
89117
* @param {string} _inverseOf Inverse property name (optional)
118+
* @param {object} _hooks Hooks to be applied just to the instantiated record
90119
* @return {BuilderApi} self
91120
*/
92-
attrAsResource: function(_attr, _model, _url, _source, _inverseOf) {
121+
attrAsResource: function(_attr, _model, _url, _source, _inverseOf, _hooks) {
122+
123+
var options, globalHooks; // global relation configuration
93124

94125
this.attrDefault(_attr, function() {
95126

96127
if(typeof _model === 'string') {
97128
_model = $injector.get(_model);
98129

130+
// retrieve global options
131+
options = _model.getProperty('hasOne', {});
132+
globalHooks = options.hooks;
133+
99134
if(_inverseOf) {
100135
var desc = _model.$$getDescription(_inverseOf);
101136
if(!desc || desc.relation !== 'belongs_to') {
@@ -105,10 +140,13 @@ RMModule.factory('RMBuilderRelations', ['$injector', 'inflector', '$log', 'RMUti
105140
}
106141
}
107142

108-
var scope = this.$buildScope(_model, _url || inflector.parameterize(_attr)),
109-
inst = _model.$new(null, scope);
143+
var scope = this.$buildScope(_model, _url || inflector.parameterize(_attr)), inst;
110144

111-
// TODO: provide a way to modify scope behavior just for this relation
145+
// setup record
146+
inst = _model.$new(null, scope);
147+
if(globalHooks) applyHooks(inst, globalHooks, this);
148+
if(_hooks) applyHooks(inst, _hooks, this);
149+
inst.$dispatch('after-has-one-init');
112150

113151
if(_inverseOf) {
114152
inst[_inverseOf] = this;
@@ -325,8 +363,8 @@ RMModule.factory('RMBuilderRelations', ['$injector', 'inflector', '$log', 'RMUti
325363
};
326364

327365
return restmod.mixin(function() {
328-
this.extend('attrAsCollection', EXT.attrAsCollection, ['hasMany', 'path', 'source', 'inverseOf']) // TODO: rename source to map, but disable attrMap if map is used here...
329-
.extend('attrAsResource', EXT.attrAsResource, ['hasOne', 'path', 'source', 'inverseOf'])
366+
this.extend('attrAsCollection', EXT.attrAsCollection, ['hasMany', 'path', 'source', 'inverseOf', 'params', 'hooks']) // TODO: rename source to map, but disable attrMap if map is used here...
367+
.extend('attrAsResource', EXT.attrAsResource, ['hasOne', 'path', 'source', 'inverseOf', 'hooks'])
330368
.extend('attrAsReference', EXT.attrAsReference, ['belongsTo', 'key', 'prefetch'])
331369
.extend('attrAsReferenceToMany', EXT.attrAsReferenceToMany, ['belongsToMany', 'keys']);
332370
});

test/relation-spec.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ describe('Restmod model relation:', function() {
4141
beforeEach(function() {
4242
Bike = restmod.model('/api/bikes', {
4343
allParts: { hasMany: 'Part' },
44-
activity: { hasMany: 'BikeRide', path: 'rides', inverseOf: 'bike' }
44+
activity: { hasMany: 'BikeRide', path: 'rides', inverseOf: 'bike' },
45+
todayActivity: { hasMany: 'BikeRide', path: 'rides', inverseOf: 'bike', params: { since: 'today' } }
4546
});
4647
});
4748

@@ -53,6 +54,49 @@ describe('Restmod model relation:', function() {
5354
expect(Bike.$new(1).activity.$url()).toEqual('/api/bikes/1/rides');
5455
});
5556

57+
it('should use parameters provided in params option', function() {
58+
Bike = restmod.model('/api/bikes', {
59+
wheels: { hasMany: 'Part', params: { category: 'wheel' } }
60+
});
61+
62+
expect(Bike.$new(1).wheels.$params.category).toEqual('wheel');
63+
});
64+
65+
it('should use hooks provided in hooks option', function() {
66+
var owner, added;
67+
Bike = restmod.model('/api/bikes').mix({
68+
wheels: {
69+
hasMany: 'Part',
70+
hooks: {
71+
'a-hook': function(_value) {
72+
owner = this.$owner;
73+
added = _value;
74+
}
75+
}
76+
}
77+
});
78+
79+
var bike = Bike.$new(1);
80+
bike.wheels.$dispatch('a-hook', ['param1']);
81+
expect(owner).toEqual(bike);
82+
expect(added).toEqual('param1');
83+
});
84+
85+
it('should trigger an after-has-many-init hook on creation', function() {
86+
var spy = jasmine.createSpy();
87+
Bike = restmod.model('/api/bikes').mix({
88+
wheels: {
89+
hasMany: 'Part',
90+
hooks: {
91+
'after-has-many-init': spy
92+
}
93+
}
94+
});
95+
96+
Bike.$new(1);
97+
expect(spy).toHaveBeenCalled();
98+
});
99+
56100
it('should set the inverse property of each child if required', function() {
57101
var bike = Bike.$new(1).$decode({ rides: [{ id: 1 }, { id: 2 }] });
58102
expect(bike.activity[0].bike).toEqual(bike);
@@ -184,6 +228,41 @@ describe('Restmod model relation:', function() {
184228
var bike = Bike.$new(1).$decode({ serial: { id: 'XX' } });
185229
expect(bike.serialNo.bike).toEqual(bike);
186230
});
231+
232+
it('should use hooks provided in hooks option', function() {
233+
var owner, added;
234+
Bike = restmod.model('/api/bikes').mix({
235+
serial: {
236+
hasOne: 'SerialNo',
237+
hooks: {
238+
'a-hook': function(_value) {
239+
owner = this.$owner;
240+
added = _value;
241+
}
242+
}
243+
}
244+
});
245+
246+
var bike = Bike.$new(1);
247+
bike.serial.$dispatch('a-hook', ['param1']);
248+
expect(owner).toEqual(bike);
249+
expect(added).toEqual('param1');
250+
});
251+
252+
it('should trigger an after-has-many-init hook on creation', function() {
253+
var spy = jasmine.createSpy();
254+
Bike = restmod.model('/api/bikes').mix({
255+
serial: {
256+
hasOne: 'SerialNo',
257+
hooks: {
258+
'after-has-one-init': spy
259+
}
260+
}
261+
});
262+
263+
Bike.$new(1);
264+
expect(spy).toHaveBeenCalled();
265+
});
187266
});
188267

189268
describe('belongsTo', function() {

0 commit comments

Comments
 (0)