Search code examples
jsdatajs-data-angular

How to make find() accept an array instead of one object?


Using JS-Data 2.6 in Angular, I'm trying to load an array of data using a nice clean URL, e.g. /report/22. (This format makes sense, because it's following business logic - I'm loading a report data by category id, and getting several rows back.)

However,

  • when I use find, then js-data dies with the dreaded [Error] "attrs" must be an object! because it expects 1 row, and cannot be convinced otherwise - unless I'm missing something.
  • when I use findAll, then I need to search using uglier /report/?categoryId = 22 - and change the server
  • when I use actions, then it appends action name to the URI, which I don't need, e.g. /report/22/myaction

Is there any way to use either find() or findAll() to

  • have a clean URL
  • load an array of data
  • preferably, not changing the server implementation

Solution

  • Everything in this answer assumes you're using the HTTP adapter.

    JSData's default expecations are the following:

    • GET /<resource>/:id returns an object, e.g. { id: 1 }
    • GET /<resource> returns an array of objects, e.g. [{ id: 1 }, { id: 2 }]
    • POST /<resource> { some: 'field' } creates a single item in your database and returns the updated item, e.g. { id: 1, some: 'field' }
    • PUT /<resource>/:id { updated: 'field' } updates a single item in your database and returns the updated item, e.g. { id: 1, updated: 'field' }
    • PUT /<resource> { updated: 'field' } updates a collection of items in your database and returns the updated items, e.g. [{ id: 1, updated: 'field' }, { id: 2, updated: 'field' }] -DELETE /<resource>/:id deletes a single item from your database
    • DELETE /<resource> deletes a collection of items from your database

    By default, DS#find does GET /<resource>/:id, and DS#findAll does GET /<resource>.

    Any response from the server needs to be in the right format by the time it gets to DS#inject.

    The lifecycle for these calls is the following:

    1. Call find or findAll method of adapter
      1. Call GET method of adapter
      2. Call HTTP method of adapter
      3. Call deserialize method on server response
    2. Pass adapter response to the afterFind or afterFindAll hook
    3. Check cacheResponse
      1. If true, pass result of afterFind or afterFindAll to DS#inject
      2. If false, pass result of afterFind or afterFindAll to DS#createInstance
    4. Return final result

    The only way to avoid an injection error is to either:

    A) Not inject the adapter response into the datastore

    or

    B) Massage the data into the correct format before it is passed into DS#inject

    DS#inject requires either an object that has a field specified by the Resource's idAttribute option, or an array of the same.

    You have three opportunities to massage the data before it gets to DS#inject:

    1. An HTTP response interceptor
    2. The deserialize hook
    3. The afterFind or afterFindAll hook

    In any one of those methods you could fix the data to be what DS#inject expects.

    If you want to be able to do Report.find(22) then you might do:

    var Report = store.defineResource({
      name: 'report',
      afterFind: function (Report, data, cb) {
        cb(null, {
          id: 22,
          data: data
        });
      }
    });
    
    Report.find(22).then(function (report) {
      report.id; // 22
      report.data; // [{...}, {...}]
    });
    

    All of this assumes you really want to be able to use DS#find, but DS#find and DS#findAll are meant to be used with RESTful resources where a Resource corresponds to a table in a database and instances of the Resource correspond to rows in the table. Generating reports is typically one of those things where you're compiling data from disparate sources, doing aggregations, etc. It's not as predictable (and hence, this question).

    Here's another option:

    var Report = store.defineResource({
      name: 'report',
    
      /* Static Class Methods */
      findByCategoryId: function (id) {
        // Use the adapter directly
        return Report.getAdapter('http').find(Report, id).then(function (data) {
          // Inject the result into the store
          return Report.inject({
            id: 22,
            data: data
          });
        });
      }
    });
    
    Report.findByCategoryId(22). then(function (report) {
      return Report.findByCategoryId(23);
    }).then(function (report) {
      Report.filter(); // [{...}, {...}]
    });
    

    Basically, there are many ways to accomplish any particular task, each with its own set of pros/cons. JSData can only generalize to so many use-cases with its default settings.

    To unlock the power of JSData and maximize your productivity, you'll want to take note of the many options and hooks in JSData that allow you to mold JSData to your liking. If you find that any particular extension or customization you've written could be generalized and benefit a lot of others who have the same use-case, let us know!

    Cheers!