1

Summary

I am using Ember-Model and I need help debugging this error message.. it is actually two separate (but similar) error messages:

TypeError: Cannot read property 'map' of undefined

-and-

Uncaught Error: Assertion Failed: TypeError: Cannot read property 'map' of undefined

Details

In my app, there is a route called "view". It pulls a collection of accounts from an API.

I have the model and custom adapter defined almost correctly and I get a successful response from the server with a JSON payload. I do not know what to do next as there is an error in the console (see above).

Somewhere in the Ember-Model source code there is a property called map inside of the materializeData function, and do not know what it is trying to do... (I see a comment in that function // FIXME which is making me nervous)


Here is my code that got me to the problem:

View Model:

import ViewAdapter from '../../adapters/accounts/view';

var attr = Ember.attr, hasMany = Ember.hasMany, belongsTo = Ember.belongsTo;

var View = Ember.Model.extend({
    debtor_id:                    attr(),
    debtor_legacy_account_number: attr(),
    debtor_full_name:             attr(),
    debtor_balance:               attr()
});

View.adapter = ViewAdapter.create();
View.url =        'api/rest/debtor/list';
View.rootKey =    'data';
View.collectionKey =  'debtors';
View.primaryKey = 'debtor_id';

export default View;

View Adapter:

var ViewAdapter = Ember.RESTAdapter.extend({
    buildURL: function(klass, id) {
        var urlRoot = Ember.get(klass, 'url');
        if (!urlRoot) { throw new Error('ViewAdapter requires a `url` property to be specified'); }
        if (!Ember.isEmpty(id)) {
            return urlRoot + "/" + id;
        } else {
            return urlRoot;
        }
    },
    ajaxSettings: function(url, method) {
        return {
            url: url,
            type: method,
            headers: {
                "Accept": "application/json; version=1.0.0"
            },
            dataType: "json"
        };
    }
});

export default ViewAdapter;

View Route:

import View from '../../models/accounts/view';

export default Ember.Route.extend({
    model: function() {
        return View.find({page_size: 10, page_number: 1});
    }
});

Note: my use of the model hook here to pass parameters to the server is temporary, I am searching a better way to do server-side pagination in a separate case... but this at least works for now...


What am I missing?


Full Stack Trace

TypeError: Cannot read property 'map' of undefined
    at Ember.RecordArray.Ember.ArrayProxy.extend.materializeData (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:60646:24)
    at Ember.RecordArray.Ember.ArrayProxy.extend.load (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:60630:31)
    at Ember.RESTAdapter.Ember.Adapter.extend.didFindQuery (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:61925:15)
    at http://local-03-02-xx.lariatcentral.net/assets/vendor.js:61916:12
    at invokeCallback (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:23709:19)
    at publish (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:23379:9)
    at publishFulfillment (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:23799:7)
    at http://local-03-02-xx.lariatcentral.net/assets/vendor.js:29217:9
    at DeferredActionQueues.invoke (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:21747:18)
    at Object.DeferredActionQueues.flush (http://local-03-02-xx.lariatcentral.net/assets/vendor.js:21797:15) vendor.js:17062
logToConsole vendor.js:17062
RSVP.onerrorDefault vendor.js:59834
__exports__.default.trigger vendor.js:22673
Promise._onerror vendor.js:23397
publishRejection vendor.js:23804
(anonymous function) vendor.js:29217
DeferredActionQueues.invoke vendor.js:21747
DeferredActionQueues.flush vendor.js:21797
Backburner.end vendor.js:21260
Backburner.run vendor.js:21315
apply vendor.js:21145
run vendor.js:19777
settings.success vendor.js:62014
fire vendor.js:3214
self.fireWith vendor.js:3326
done vendor.js:9370
callback

JSON Payload Sample

{
  "status":"success",
  "data":{
    "debtors":[
      {
        "debtor_id":1048,
        // ...
        // ...
      },
      {
        "debtor_id":1049,
        // ...
        // ...
      },
      {
        "debtor_id":1050,
        // ...
        // ...
      },
      // ...more JSON...
    ],
    "count":10,
    "total":475,
    "current_page":1,
    "total_pages":48,
    "page_size":10
  }
}

I also found this github issue that looks exactly like my problem.

Grapho
  • 1,654
  • 15
  • 33

1 Answers1

3

When you call View.find({page_size: 10, page_number: 1}); ember-model will send a request to the server, get the payload and call the didFindQuery hook. Since you're using the RESTAdapter the current implemantation is the following:

didFindQuery: function(klass, records, params, data) {
  var collectionKey = Ember.get(klass, 'collectionKey'),
    dataToLoad = collectionKey ? data[collectionKey] : data;

  records.load(klass, dataToLoad);
},

The fourth param data is your payload. And because you setup the View.collectionKey to 'debtors' it will call records.load with data['debtors'] which returns undefined. That undefined value is passed to materializeData and you're receiving the TypeError: Cannot read property 'map' of undefined. What you need is data['data']['debtors'].

What you can do is:

Change your payload structure to:

{
  "status":"success",
  "debtors":[
      {
        "debtor_id":1048,
        // ...
        // ...
      },
      {
        "debtor_id":1049,
        // ...
        // ...
      },
      {
        "debtor_id":1050,
        // ...
        // ...
      },
      // ...more JSON...
    ],
    "count":10,
    "total":475,
    "current_page":1,
    "total_pages":48,
    "page_size":10
  }
}

Or override the didFindQuery and didFindAll to:

var ViewAdapter = Ember.RESTAdapter.extend({
    // others methods ...
    didFindQuery: function(klass, records, params, data) {
      var collectionKey = Ember.get(klass, 'collectionKey'),
        dataToLoad = collectionKey ? data['data'][collectionKey] : data;

      records.load(klass, dataToLoad);
    },
    didFindAll: function(klass, records, data) {
      var collectionKey = Ember.get(klass, 'collectionKey'),
        dataToLoad = collectionKey ? data['data'][collectionKey] : data;

      records.load(klass, dataToLoad);
    }
})

I hope it helps

Marcio Junior
  • 19,078
  • 4
  • 44
  • 47
  • Thank you for the description of how it is supposed to work! I definitely need to override the Adapter because I can't change the API. I will try it out and see if I can get it to work now. So... looking back, was Ember-Model getting confused between the `data` rootKey and the `data` payload parameter having the same name? – Grapho Aug 07 '14 at 17:10
  • Ember-model expect the whole payload to be the model data, or the value of `collectionKey` if present. But it goes just one level, what you need is something like data `View.collectionKey = 'data.debtors';`, because your model data is two level depth. Since this syntax isn't allowed you need to override the adapter. – Marcio Junior Aug 07 '14 at 17:26
  • I think your solution almost did the trick, but i can't get an `{{#each}}` to work? Ember inspector shows no model or records? – Grapho Aug 07 '14 at 22:25
  • I updated the answer to use `Ember.get` instead of `get` maybe this was causing your problem. I also created a [jsbin here](http://emberjs.jsbin.com/OxIDiVU/921/edit) with your code and all works. Can you show how you are using the `{{#each}}` in your code? – Marcio Junior Aug 09 '14 at 16:43
  • you are right! I was doing it wrong with the `{{#each}}` block.... for some reason I thought I could write it out verbosely like: `each debtors in View`, but i guess i was wrong.... what is the difference between `items in model` and the way I was trying to do it? ...also, is there a possibility of only using the `{{debtor_name}}` instead of `{{item.debtor_name}}` or is that just how ember works? – Grapho Aug 11 '14 at 14:35
  • 1
    The context of the template is the controller. So if you do `{{#each item in controller}}` or `{{#each item in this}}`, you will get the same result. Removing the variable from the `#each` will make the context of the `#each` block be each element, for instance `{{#each controller}}{{debtor_full_name}}{{/each}}`. Because this pattern is so common, ember provides a `{{#each}}{{debtor_full_name}}{{/each}}`, the empty `#each` will be expanded to `#each controller`. – Marcio Junior Aug 11 '14 at 17:09