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 to false. You can set it to true 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 in for (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:

📘

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.