Skip to content

Commit 100d868

Browse files
committed
Find, create, update and destroy now support nested endpoints. #40
1 parent 0ea37ba commit 100d868

19 files changed

+355
-38
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
##### 1.0.0-beta.1 - xx August 2014
22

33
###### Backwards compatible API changes
4+
- #40 - Support for nested resource endpoints
45
- #118, #122 - Multiple relationships to the same model
56
- #120 - When using DSCacheFactory, allow onExpire to be specified
67

dist/angular-data.js

+46-10
Original file line numberDiff line numberDiff line change
@@ -1794,7 +1794,7 @@ function DSHttpAdapterProvider() {
17941794
function create(resourceConfig, attrs, options) {
17951795
options = options || {};
17961796
return this.POST(
1797-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
1797+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(attrs, options)),
17981798
attrs,
17991799
options
18001800
);
@@ -1803,7 +1803,7 @@ function DSHttpAdapterProvider() {
18031803
function destroy(resourceConfig, id, options) {
18041804
options = options || {};
18051805
return this.DEL(
1806-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id),
1806+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(id, options), id),
18071807
options
18081808
);
18091809
}
@@ -1816,15 +1816,15 @@ function DSHttpAdapterProvider() {
18161816
DSUtils.deepMixIn(options.params, params);
18171817
}
18181818
return this.DEL(
1819-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
1819+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(null, options)),
18201820
options
18211821
);
18221822
}
18231823

18241824
function find(resourceConfig, id, options) {
18251825
options = options || {};
18261826
return this.GET(
1827-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id),
1827+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(id, options), id),
18281828
options
18291829
);
18301830
}
@@ -1837,15 +1837,15 @@ function DSHttpAdapterProvider() {
18371837
DSUtils.deepMixIn(options.params, params);
18381838
}
18391839
return this.GET(
1840-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
1840+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(null, options)),
18411841
options
18421842
);
18431843
}
18441844

18451845
function update(resourceConfig, id, attrs, options) {
18461846
options = options || {};
18471847
return this.PUT(
1848-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id),
1848+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(id), id),
18491849
attrs,
18501850
options
18511851
);
@@ -1859,7 +1859,7 @@ function DSHttpAdapterProvider() {
18591859
DSUtils.deepMixIn(options.params, params);
18601860
}
18611861
return this.PUT(
1862-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
1862+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(null, options)),
18631863
attrs,
18641864
options
18651865
);
@@ -4507,6 +4507,7 @@ var methodsToProxy = [
45074507
*/
45084508
function defineResource(definition) {
45094509
var DS = this;
4510+
var definitions = DS.definitions;
45104511
var IA = DS.errors.IA;
45114512

45124513
if (DS.utils.isString(definition)) {
@@ -4530,9 +4531,43 @@ function defineResource(definition) {
45304531
try {
45314532
// Inherit from global defaults
45324533
Resource.prototype = DS.defaults;
4533-
DS.definitions[definition.name] = new Resource(DS.utils, definition);
4534+
definitions[definition.name] = new Resource(DS.utils, definition);
4535+
4536+
var def = definitions[definition.name];
45344537

4535-
var def = DS.definitions[definition.name];
4538+
// Setup nested parent configuration
4539+
if (def.relations && def.relations.belongsTo) {
4540+
DS.utils.forOwn(def.relations.belongsTo, function (relatedModel, modelName) {
4541+
if (!DS.utils.isArray(relatedModel)) {
4542+
relatedModel = [relatedModel];
4543+
}
4544+
DS.utils.forEach(relatedModel, function (relation) {
4545+
if (relation.parent) {
4546+
def.parent = modelName;
4547+
def.parentKey = relation.localKey;
4548+
}
4549+
});
4550+
});
4551+
}
4552+
4553+
def.getEndpoint = function (attrs, options) {
4554+
var parent = this.parent;
4555+
var parentKey = this.parentKey;
4556+
options = options || {};
4557+
if (!('nested' in options)) {
4558+
options.nested = true;
4559+
}
4560+
if (parent && parentKey && definitions[parent] && options.nested) {
4561+
if (DS.utils.isObject(attrs) && parentKey in attrs) {
4562+
return DS.utils.makePath(definitions[parent].getEndpoint(attrs, options), attrs[parentKey], this.endpoint);
4563+
} else if ((DS.utils.isNumber(attrs) || DS.utils.isString(attrs)) && DS.get(this.name, attrs) && parentKey in DS.get(this.name, attrs)) {
4564+
return DS.utils.makePath(definitions[parent].getEndpoint(attrs, options), DS.get(this.name, attrs)[parentKey], this.endpoint);
4565+
} else if (options && options.parentKey) {
4566+
return DS.utils.makePath(definitions[parent].getEndpoint(attrs, options), options.parentKey, this.endpoint);
4567+
}
4568+
}
4569+
return this.endpoint;
4570+
};
45364571

45374572
// Remove this in v0.11.0 and make a breaking change notice
45384573
// the the `filter` option has been renamed to `defaultFilter`
@@ -4633,7 +4668,7 @@ function defineResource(definition) {
46334668
return def;
46344669
} catch (err) {
46354670
DS.$log.error(err);
4636-
delete DS.definitions[definition.name];
4671+
delete definitions[definition.name];
46374672
delete DS.store[definition.name];
46384673
throw err;
46394674
}
@@ -5866,6 +5901,7 @@ module.exports = [function () {
58665901
pascalCase: require('mout/string/pascalCase'),
58675902
deepMixIn: require('mout/object/deepMixIn'),
58685903
forOwn: require('mout/object/forOwn'),
5904+
forEach: angular.forEach,
58695905
pick: require('mout/object/pick'),
58705906
set: require('mout/object/set'),
58715907
contains: require('mout/array/contains'),

dist/angular-data.min.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

guide/angular-data/resource/resource.md

+45-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,12 @@ DS.defineResource({
301301
belongsTo: {
302302
organization: {
303303
localKey: 'organizationId',
304-
localField: 'organization'
304+
localField: 'organization',
305+
306+
// if you add this to a belongsTo relation
307+
// then angular-data will attempt to use
308+
// a nested url structure, e.g. /organization/15/user/4
309+
parent: true
305310
}
306311
}
307312
}
@@ -413,6 +418,45 @@ You can configure your server to also return the `comment` and `organization` re
413418
414419
If you've told angular-data about the relations, then the comments and organization will be injected into the data store in addition to the user.
415420
421+
## Nested Resource Endpoints
422+
423+
Add `parent: true` to a belongsTo relationship to activate nested resource endpoints for the resource. Angular-data will attempt to find the appropriate key in order to build the url. If the parent key cannot be found then angular-data will resort to a non-nested url unless you manually provide the id of the parent.
424+
425+
Example:
426+
427+
```js
428+
DS.defineResource({
429+
name: 'comment',
430+
relations: {
431+
belongsTo: {
432+
post: {
433+
parent: true,
434+
localKey: 'postId',
435+
localField: 'post'
436+
}
437+
}
438+
}
439+
});
440+
441+
// The comment isn't in the data store yet, so angular-data wouldn't know
442+
// what the id of the parent "post" would be, so we pass it in manually
443+
DS.find('comment', 5, { parentKey: 4 }); // GET /post/4/comment/5
444+
445+
// vs
446+
447+
DS.find('comment', 5); // GET /comment/5
448+
449+
DS.inject('comment', { id: 1, postId: 2 });
450+
451+
// We don't have to provide the parentKey here
452+
// because angular-data found it in the comment
453+
DS.update('comment', 1, { content: 'stuff' }); // PUT /post/2/comment/1
454+
455+
// If you don't want the nested for just one of the calls then
456+
// you can do the following:
457+
DS.update('comment', 1, { content: 'stuff' }, { nested: false ); // PUT /comment/1
458+
```
459+
416460
@doc overview
417461
@id computed
418462
@name Computed Properties

karma.start.js

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ function startInjector() {
190190
localKey: 'userId'
191191
},
192192
{
193+
parent: true,
193194
localField: 'approvedByUser',
194195
localKey: 'approvedBy'
195196
}

src/adapters/http.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ function DSHttpAdapterProvider() {
376376
function create(resourceConfig, attrs, options) {
377377
options = options || {};
378378
return this.POST(
379-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
379+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(attrs, options)),
380380
attrs,
381381
options
382382
);
@@ -385,7 +385,7 @@ function DSHttpAdapterProvider() {
385385
function destroy(resourceConfig, id, options) {
386386
options = options || {};
387387
return this.DEL(
388-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id),
388+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(id, options), id),
389389
options
390390
);
391391
}
@@ -398,15 +398,15 @@ function DSHttpAdapterProvider() {
398398
DSUtils.deepMixIn(options.params, params);
399399
}
400400
return this.DEL(
401-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
401+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(null, options)),
402402
options
403403
);
404404
}
405405

406406
function find(resourceConfig, id, options) {
407407
options = options || {};
408408
return this.GET(
409-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id),
409+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(id, options), id),
410410
options
411411
);
412412
}
@@ -419,15 +419,15 @@ function DSHttpAdapterProvider() {
419419
DSUtils.deepMixIn(options.params, params);
420420
}
421421
return this.GET(
422-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
422+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(null, options)),
423423
options
424424
);
425425
}
426426

427427
function update(resourceConfig, id, attrs, options) {
428428
options = options || {};
429429
return this.PUT(
430-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint, id),
430+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(id), id),
431431
attrs,
432432
options
433433
);
@@ -441,7 +441,7 @@ function DSHttpAdapterProvider() {
441441
DSUtils.deepMixIn(options.params, params);
442442
}
443443
return this.PUT(
444-
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.endpoint),
444+
DSUtils.makePath(options.baseUrl || resourceConfig.baseUrl, resourceConfig.getEndpoint(null, options)),
445445
attrs,
446446
options
447447
);

src/datastore/sync_methods/defineResource.js

+38-3
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ var methodsToProxy = [
101101
*/
102102
function defineResource(definition) {
103103
var DS = this;
104+
var definitions = DS.definitions;
104105
var IA = DS.errors.IA;
105106

106107
if (DS.utils.isString(definition)) {
@@ -124,9 +125,43 @@ function defineResource(definition) {
124125
try {
125126
// Inherit from global defaults
126127
Resource.prototype = DS.defaults;
127-
DS.definitions[definition.name] = new Resource(DS.utils, definition);
128+
definitions[definition.name] = new Resource(DS.utils, definition);
128129

129-
var def = DS.definitions[definition.name];
130+
var def = definitions[definition.name];
131+
132+
// Setup nested parent configuration
133+
if (def.relations && def.relations.belongsTo) {
134+
DS.utils.forOwn(def.relations.belongsTo, function (relatedModel, modelName) {
135+
if (!DS.utils.isArray(relatedModel)) {
136+
relatedModel = [relatedModel];
137+
}
138+
DS.utils.forEach(relatedModel, function (relation) {
139+
if (relation.parent) {
140+
def.parent = modelName;
141+
def.parentKey = relation.localKey;
142+
}
143+
});
144+
});
145+
}
146+
147+
def.getEndpoint = function (attrs, options) {
148+
var parent = this.parent;
149+
var parentKey = this.parentKey;
150+
options = options || {};
151+
if (!('nested' in options)) {
152+
options.nested = true;
153+
}
154+
if (parent && parentKey && definitions[parent] && options.nested) {
155+
if (DS.utils.isObject(attrs) && parentKey in attrs) {
156+
return DS.utils.makePath(definitions[parent].getEndpoint(attrs, options), attrs[parentKey], this.endpoint);
157+
} else if ((DS.utils.isNumber(attrs) || DS.utils.isString(attrs)) && DS.get(this.name, attrs) && parentKey in DS.get(this.name, attrs)) {
158+
return DS.utils.makePath(definitions[parent].getEndpoint(attrs, options), DS.get(this.name, attrs)[parentKey], this.endpoint);
159+
} else if (options && options.parentKey) {
160+
return DS.utils.makePath(definitions[parent].getEndpoint(attrs, options), options.parentKey, this.endpoint);
161+
}
162+
}
163+
return this.endpoint;
164+
};
130165

131166
// Remove this in v0.11.0 and make a breaking change notice
132167
// the the `filter` option has been renamed to `defaultFilter`
@@ -227,7 +262,7 @@ function defineResource(definition) {
227262
return def;
228263
} catch (err) {
229264
DS.$log.error(err);
230-
delete DS.definitions[definition.name];
265+
delete definitions[definition.name];
231266
delete DS.store[definition.name];
232267
throw err;
233268
}

src/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = [function () {
1313
pascalCase: require('mout/string/pascalCase'),
1414
deepMixIn: require('mout/object/deepMixIn'),
1515
forOwn: require('mout/object/forOwn'),
16+
forEach: angular.forEach,
1617
pick: require('mout/object/pick'),
1718
set: require('mout/object/set'),
1819
contains: require('mout/array/contains'),

test/integration/adapters/http/create.test.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ describe('DSHttpAdapter.create(resourceConfig, attrs, options)', function () {
1010

1111
DSHttpAdapter.create({
1212
baseUrl: 'api',
13-
endpoint: 'posts'
13+
endpoint: 'posts',
14+
getEndpoint: function () {
15+
return 'posts';
16+
}
1417
}, { author: 'John', age: 30 }).then(function (data) {
1518
assert.deepEqual(data.data, p1, 'post should have been created');
1619
}, function (err) {
@@ -27,7 +30,10 @@ describe('DSHttpAdapter.create(resourceConfig, attrs, options)', function () {
2730

2831
DSHttpAdapter.create({
2932
baseUrl: 'api',
30-
endpoint: 'posts'
33+
endpoint: 'posts',
34+
getEndpoint: function () {
35+
return 'posts';
36+
}
3137
}, { author: 'John', age: 30 }, { baseUrl: 'api2' }).then(function (data) {
3238
assert.deepEqual(data.data, p1, 'post should have been created');
3339
}, function (err) {

test/integration/adapters/http/destroy.test.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ describe('DSHttpAdapter.destroy(resourceConfig, id, options)', function () {
77

88
DSHttpAdapter.destroy({
99
baseUrl: 'api',
10-
endpoint: 'posts'
10+
endpoint: 'posts',
11+
getEndpoint: function () {
12+
return 'posts';
13+
}
1114
}, 1).then(function (data) {
1215
assert.deepEqual(data.data, 1, 'post should have been deleted');
1316
}, function (err) {
@@ -21,7 +24,10 @@ describe('DSHttpAdapter.destroy(resourceConfig, id, options)', function () {
2124

2225
DSHttpAdapter.destroy({
2326
baseUrl: 'api',
24-
endpoint: 'posts'
27+
endpoint: 'posts',
28+
getEndpoint: function () {
29+
return 'posts';
30+
}
2531
}, 1, { baseUrl: 'api2' }).then(function (data) {
2632
assert.deepEqual(data.data, 1, 'post should have been deleted');
2733
}, function (err) {

0 commit comments

Comments
 (0)