Relations
Resource Relations
JSData 1.x => 2.x Relation API Change
Starting with JS-Data 2.0, relations are linked to instances via property accessors. See Object.defineProperty. By default, relations will now have
enumerable
set tofalse
. You can set it totrue
in your relations definitions if you want, but I don't recommend it.Without enumerability, linked relations won't show up in
console.log
or infor (var key in user)
, etc. (They also won't cause cyclic object issues.)See js-data#107 and below for more info.
For more examples of relations, see the examples page.
JSData supports relations. If GET /user/10
returns:
{
id: 10,
name: 'John Anderson',
profile: {
email: 'John Anderson',
id: 18,
userId: 10
}
}
then only the user object is injected into the store. If you've defined the hasOne
relationship of users to profiles (or belongsTo of profile to user), not only will the user be injected into the store, but the profile as well (into its own part of the store).
Examples:
Without defining relations:
var userJson = {
id: 10,
name: 'John Anderson',
profile: {
email: 'John Anderson',
id: 18,
userId: 10
}
};
User.inject(userJson);
assert.deepEqual(User.get(10), userJson);
assert.isUndefined(Profile.get(18));
With relations defined:
var userJson = {
id: 10,
name: 'John Anderson',
profile: {
email: 'John Anderson',
id: 18,
userId: 10
}
};
User.inject(userJson);
assert.deepEqual(User.get(10), userJson);
assert.deepEqual(Profile.get(18), userJson.profile);
assert.deepEqual(Profile.get(18), User.get(10).profile);
Defining relations:
var User = store.defineResource({
name: 'user',
relations: {
// hasMany uses "localField" and "localKeys" or "foreignKey"
hasMany: {
comment: {
// localField is for linking relations
// user.comments -> array of comments of the user
localField: 'comments',
// foreignKey is the "join" field
// the name of the field on a comment that points to its parent user
foreignKey: 'userId'
}
},
// hasOne uses "localField" and "localKey" or "foreignKey"
hasOne: {
profile: {
// localField is for linking relations
// user.profile -> profile of the user
localField: 'profile',
// foreignKey is the "join" field
// the name of the field on a profile that points to its parent user
foreignKey: 'userId'
}
},
// belongsTo uses "localField" and "localKey"
belongsTo: {
organization: {
// localField is for linking relations
// user.organization -> organization of the user
localField: 'organization',
// localKey is the "join" field
// the name of the field on a user that points to its parent organization
localKey: 'organizationId',
// if you add this to a belongsTo relation
// then js-data will attempt to use
// a nested url structure, e.g. /organization/15/user/4
parent: true
}
}
}
});
var Organization = store.defineResource({
name: 'organization',
relations: {
hasMany: {
// this is an example of multiple relations
// of the same type to the same resource
user: [
{
// localField is for linking relations
// organization.users -> array of users of the organization
localField: 'users',
// foreignKey is the "join" field
// the name of the field on a user that points to its parent organization
foreignKey: 'organizationId'
},
{
// localField is for linking relations
// organization.owners -> array of users of the organization
localField: 'owners',
// foreignKey is the "join" field
// the name of the field on a user that points to its parent organization
foreignKey: 'organizationId'
}
]
}
}
});
var Profile = store.defineResource({
name: 'profile',
relations: {
belongsTo: {
user: {
// localField is for linking relations
// profile.user -> user of the profile
localField: 'user',
// localKey is the "join" field
// the name of the field on a profile that points to its parent user
localKey: 'userId'
}
}
}
});
var Comment = store.defineResource({
name: 'comment',
relations: {
belongsTo: {
user: {
// localField is for linking relations
// comment.user -> user of the profile
localField: 'user',
// localKey is the "join" field
// the name of the field on a comment that points to its parent user
localKey: 'userId'
}
}
}
});
You can manually load items into the data store via DS#inject
.
Enumerable relations
var Comment = store.defineResource({
name: 'comment',
relations: {
belongsTo: {
user: {
// localField is for linking relations
// comment.user -> user of the profile
localField: 'user',
// localKey is the "join" field
// the name of the field on a comment that points to its parent user
localKey: 'userId',
// comment.user will show up in console.log and key enumeration now
enumerable: true
}
}
}
});
Custom relation getters
What's this? You can customize how relations are linked.
Show me:
Annotated example
var User = store.defineResource({
name: 'user',
relations: {
hasMany: {
comment: {
// a user's comments will be found at user.comments
localField: 'comments',
// the field on a comment that points to its user
foreignKey: 'userId',
// custom getter
// this is what you came to see
// it receives 4 arguments
// - Resource (The Resource for the starting point of this relation)
// - relationDef (meta data about the relation)
// - instance (The instance of User that the getter is being invoked for)
// - origGetter (The original getter function)
get: function (Resource, relationDef, instance, origGetter) {
Resource === User; // true
instance === this; // true
relationDef.name; // "user"
relationDef.type; // "hasMany"
relationDef.relation; // "comment"
typeof origGetter; // "function"
// here, do whatever you want:
// - broadcast a message
// - change some data
// - return some comments instead of using the original getter
// - etc
// if you still just want to use the original getter, do this
return origGetter();
}
}
}
}
});
Loading relations
Lazy
User.find(10).then(function (user) {
// let's assume the server only returned the user
user.comments; // undefined
user.profile; // undefined
return User.loadRelations(user, ['comment', 'profile']);
}).then(function (user) {
user.comments; // array
user.profile; // object
});
Live Demos
Direct (requires server-side support)
When you call DS#find
of DS#findAll
you can pass params
to the http adapter (if you're using it) like so:
var params = {...};
var options = {
// Also serialized to the query string, but otherwise ignored by js-data
// Only applies to the http adapter
params: {}
};
User.find(10, options);
User.findAll(params, options);
You could, for example, configure your server to look for a query string parameter called with
or something, which tells the server which relations to include with the response, for example:
var params = {...};
// When using the http adapter, send the "with" option as part of the query string
// Your server will need to know what to do with it
var options = {
// Will be serialized to the query string, but otherwise ignored by js-data
// Only applies to the http adapter
params: {
with: ['comment', 'organization']
}
};
// GET /user/10?with=comment&with=organization
User.find(10, options);
// GET /user?with=comment&with=organization&other=params&go=here
User.findAll(params, options);
All the other adapters (not http) understand the with
option directly:
var params = {...};
// When NOT using the http adapter, put "with" right on the options
var options = {
with: ['comment', 'organization']
};
User.find(10, options);
User.findAll(params, options);
If using the http adapter, you can configure your server to return the comment
and organization
relations in the response:
{
id: 10,
name: 'John Anderson',
organizationId: 15,
comments: [...],
organization: {...}
}
With all the other adapter, they'll automatically figure out how to grab those nested relations out of their persistence layer.
If you've told js-data about the relations, then the comments and organization will be injected into the data store in addition to the user.
Nested Resource Endpoints
Add parent: true
to a belongsTo relationship to activate nested resource endpoints for the resource. Js-data will attempt to find the appropriate key in order to build the url. If the parent key cannot be found then js-data will resort to a non-nested url unless you manually provide the id of the parent.
Example:
var Comment = store.defineResource({
name: 'comment',
relations: {
belongsTo: {
post: {
parent: true,
localKey: 'postId',
localField: 'post'
}
}
}
});
// The comment isn't in the data store yet, so js-data wouldn't know
// what the id of the parent "post" would be, so we pass it in manually
Comment.find(5, { params: { postId: 4 } }); // GET /post/4/comment/5
// vs
Comment.find(5); // GET /comment/5
Comment.inject({ id: 1, postId: 2 });
// We don't have to provide the parentKey here
// because js-data found it in the comment
Comment.update(1, { content: 'stuff' }); // PUT /post/2/comment/1
// If you don't want the nested for just one of the calls then
// you can do the following:
Comment.update(1, { content: 'stuff' }, { params: { postId: false } }); // PUT /comment/1
Additional reading:
- More relations examples
- Resources
- Model Lifecycle Hooks
- Computed Properties
- Instance Methods (Custom instance behavior)
Need help?
Want more examples or have a question? Post on the Slack channel or mailing list then we'll get your question answered and probably update this wiki.
Updated less than a minute ago