Search code examples
jsdata

JSData load relations without opts


When eagerly loading relations through a mapper, the opts argument is passed down to the loaded relations. This breaks the api in my case. For instance:

storyMapper.findAll({ title: 'foobar' }, { with: ['user'] });

This results in two requests:

GET /stories?title=foobar
GET /users?title=foobar

I might be missing something, but I would expect the defined relations to be used so that the story is loaded first, it's userId field read, and the second query be something like

GET /users/<the id>

Or at least

GET /users?where=<id in <the id>>

So my question is; can i change the behavior of this or do I need to use loadRelations on each story after it has loaded?


Code samples:

// user schema

import { Schema } from 'js-data';

export const user = new Schema({
    $schema: 'http://json-schema.org/draft-04/schema#',
    title: 'User',
    description: 'Schema for User records',
    type: 'object',
    properties: {
        _id: { type: 'string' },
        username: { type: 'string' },
        email: { type: 'string' },
        password: { type: 'string' },
    },

    required: ['username', 'email', 'password'],
});

// story schema

import { Schema } from 'js-data';

export const story = new Schema({
    $schema: 'http://json-schema.org/draft-04/schema#',
    title: 'Story',
    description: 'Schema for Story records',
    type: 'object',
    properties: {
        _id: { type: 'string' },
        title: { type: 'string', default: '' },
        userId: { type: ['string', 'null'] },
        username: { type: ['string', 'null'] },
    },
    required: ['title'],
});

// user mapper

this.store.defineMapper('user', {
    mapperClass: ObservableMapper,
    recordClass: User,
    endpoint: 'users',
    idAttribute: '_id',
    schema: schemas.user,
    relations: relations.user,
})

// story mapper

this.store.defineMapper('story', {
    mapperClass: ObservableMapper,
    recordClass: Story,
    endpoint: 'storys',
    idAttribute: '_id',
    schema: schemas.story,
    relations: relations.story,
})

// user relations

export const user = {
    hasMany: {
        world: {
            foreignKey: 'userId',
            localField: 'worlds',
        },
    },
};

// story relations

export const world = {
    belongsTo: {
        user: {
            foreignKey: 'userId',
            localField: 'user',
        },
    },
};

Sample data returned from GET /stories?title=foobar:

{
  "_id": "546e53dcedee82d542000003",
  "userId": "526e8617964fd22d2b000001",
  "username": "Someone",
  "title": "Lorem Ipsum"
}

Solution

  • You're missing the other side of the User-Story relation:

    // user relations
    export const user = {
      hasMany: {
        story: {
          foreignKey: 'userId',
          localField: 'stories'
        },
        world: {
          foreignKey: 'userId',
          localField: 'worlds'
        }
      }
    };
    

    Now, when you actually make the request you have two options:

    Option 1 - Multiple requests

    This requires that your server understands the "where" querystring parameter:

    store.findAll('story', { title: 'foobar' }, { with: ['user'] })
    

    Here's a plunker that demonstrates: https://plnkr.co/edit/UCFJNg?p=preview

    The plunker example makes two requests:

    • GET /stories?title=foobar
    • GET /users?title=foobar&where={"_id":{"in":[123,234]}}

    Option 2 - A single request

    This requires that your server understands the "with" querystring parameter:

    store.findAll('story', { title: 'foobar' }, { params: { with: ['user'] } })
    

    Here's a plunker that demonstrates: https://plnkr.co/edit/M6quP4?p=preview

    The plunker example makes just one request, and expects the users to be embedded within the stories in the server's response:

    • GET /stories?with=user&title=foobar

    Note

    This is a quirk of the HTTP adapter. For all other adapters, using the with option works as you'd expect, and you don't have to mess with the params option.