Looking for help with properly calling json using Ember Data

Howdy y’all, throwing a Hail Mary out here hoping someone has time to help. I’ve been working on cloning a glossary for quite some time. I feel like I’m really close to getting this right, but I don’t know enough about how to use Ember Data to get the appropriate response from my json. I kept running into issues where Ember Data was looking for http://myjson.com/gd3th/words when I used myson. After a week of failing to call that properly, I decided to fall back to using the mirage/config.js file like you do in the Super Rentals tutorial. Now I’m running into Error while processing route: words Assertion Failed: id passed to findRecord() has to be non-empty string or number and don’t know where to go from here.

Would any of you be kind enough to clone and/or look at this repo and tell me what I’m doing wrong?

https://github.com/jameshahn2/glossary-app.git

For starters I’d move this array to mirage fixtures (see here, you’ll need to call loadFixtures in your default scenario).

But now on to the actual issue… this one is super common but kinda confusing. Basically it means that Ember Data was trying to serialize a record, and the “record” that it was trying to serialize wasn’t in the format it expected so it choked. This can happen for many reasons but usually it means the response that the Ember app is receiving from the server is malformed somehow. I haven’t run the code yet but just looking at the mirage config my guess is that it’s because you’re wrapping the response in { data: <stuff> }. Mirage does this automatically via the mirage serializer that you define, so this is probably happening twice and Ember is receiving something like:

{ data: { data: [...] } }

So try returning just an array from your mirage config routes and see what happens.

1 Like

Tried the fixture and I kept getting TypeError: (void 0) is undefined. However, after I put the array back over to mirage/config.js it seems what I added to mirage/scenarios/default.js helped… to an extent. So I now how…

export default function(server) {
server.loadFixtures("words");
}

Now getting Error while processing route: words Assertion Failed: Encountered a resource object with an undefined type.

I should probably back up a bit. Mirage has its own ORM and it looks at your ember models to determine its own mirage models. It creates an internal database and schema, and it uses information from that schema to do proper serialization of records. So the reason I suggested using fixtures is that the way you currently have your mirage code you’re skirting the mirage ORM which can work but it means you have to do all the work that mirage is already trying to do yourself.

So… I’d suggest loading the data as fixtures into the mirage db. It might take a little playing with the syntax but it’s worth it. Then in your route handlers you’ll want to look use the schema to look up the words instead of the array:

this.get('/words/:word', function ({ words }, request) {
  return words.findBy({ word: request.params.word });
});
2 Likes

Am I understanding this correctly that this:

        {
            "id": "1",
            "url": "https://www.glossary.oilfield.slb.com/en/Terms/o/octahedral_layer.aspx",
            "word": "octahedral layer",
            "letter": "o",
            "definitions": [
              {
                "speech_type": "1. n.",
                "category": "Drilling Fluids",
                "definition": "<div class=\"definition-text\">\n One of the layers that constitute the atomic\n <a href=\"/en/Terms/s/structure.aspx\">\n  structure\n </a>\n of the\n <a href=\"/en/Terms/c/clay.aspx\">\n  clay\n </a>\n <a href=\"/en/Terms/g/group.aspx\">\n  group\n </a>\n of layered\n <a href=\"/en/Terms/s/silicate.aspx\">\n  silicate\n </a>\n minerals. The structure of these minerals can consist of two, three or four layers. The octahedral\n <a href=\"/en/Terms/l/layer.aspx\">\n  layer\n </a>\n is a plane of aluminum hydroxide octahedra (aluminum at the center and hydroxides at all six corners). Another\n <a href=\"/en/Terms/s/structural.aspx\">\n  structural\n </a>\n layer is a plane of silicon dioxide tetrahedra (silicon at the center and oxygen at all four corners of the tetrahedron). The tetrahedral and octahedral layers fit one on top of the other, with oxygen atoms being shared as oxide and hydroxide groups.\n</div>\n",
                "see": [
                  {
                    "title": "bentonite",
                    "link": "https://www.glossary.oilfield.slb.com/en/Terms/b/bentonite.aspx"
                  },
                  {
                    "title": "silica layer",
                    "link": "https://www.glossary.oilfield.slb.com/en/Terms/s/silica_layer.aspx"
                  },
                  {
                    "title": "tetrahedral layer",
                    "link": "https://www.glossary.oilfield.slb.com/en/Terms/t/tetrahedral_layer.aspx"
                  }
                ],
                "more_details": [

                ],
                "synonyms": [

                ],
                "antonyms": [

                ]
              }
            ]
          }

Becomes this?

{ id: 1, url: "https://www.glossary.oilfield.slb.com/en/Terms/o/octahedral_layer.aspx", word: "octahedral layer", letter: "o", speechType: "1. n.", category: "Drilling Fluids", definition: "<div class=\"definition-text\">\n One of the layers that constitute the atomic\n <a href=\"/en/Terms/s/structure.aspx\">\n  structure\n </a>\n of the\n <a href=\"/en/Terms/c/clay.aspx\">\n  clay\n </a>\n <a href=\"/en/Terms/g/group.aspx\">\n  group\n </a>\n of layered\n <a href=\"/en/Terms/s/silicate.aspx\">\n  silicate\n </a>\n minerals. The structure of these minerals can consist of two, three or four layers. The octahedral\n <a href=\"/en/Terms/l/layer.aspx\">\n  layer\n </a>\n is a plane of aluminum hydroxide octahedra (aluminum at the center and hydroxides at all six corners). Another\n <a href=\"/en/Terms/s/structural.aspx\">\n  structural\n </a>\n layer is a plane of silicon dioxide tetrahedra (silicon at the center and oxygen at all four corners of the tetrahedron). The tetrahedral and octahedral layers fit one on top of the other, with oxygen atoms being shared as oxide and hydroxide groups.\n</div>\n", see: [ title: "bentonite", link: "https://www.glossary.oilfield.slb.com/en/Terms/b/bentonite.aspx", title: "silica layer", link: "https://www.glossary.oilfield.slb.com/en/Terms/s/silica_layer.aspx", title: "tetrahedral layer", link: "https://www.glossary.oilfield.slb.com/en/Terms/t/tetrahedral_layer.aspx" ] }

Do you mean in the fixture file? You should be able to paste it in basically just as you have it in the config file e.g.:

export default [
        {
            "id": "1",
            "url": "https://www.glossary.oilfield.slb.com/en/Terms/o/octahedral_layer.aspx",
            "word": "octahedral layer",
            "letter": "o",
            "definitions": [
              {
                "speech_type": "1. n.",
                "category": "Drilling Fluids",
                "definition": "<div class=\"definition-text\">\n One of the layers that constitute the atomic\n <a href=\"/en/Terms/s/structure.aspx\">\n  structure\n </a>\n of the\n <a href=\"/en/Terms/c/clay.aspx\">\n  clay\n </a>\n <a href=\"/en/Terms/g/group.aspx\">\n  group\n </a>\n of layered\n <a href=\"/en/Terms/s/silicate.aspx\">\n  silicate\n </a>\n minerals. The structure of these minerals can consist of two, three or four layers. The octahedral\n <a href=\"/en/Terms/l/layer.aspx\">\n  layer\n </a>\n is a plane of aluminum hydroxide octahedra (aluminum at the center and hydroxides at all six corners). Another\n <a href=\"/en/Terms/s/structural.aspx\">\n  structural\n </a>\n layer is a plane of silicon dioxide tetrahedra (silicon at the center and oxygen at all four corners of the tetrahedron). The tetrahedral and octahedral layers fit one on top of the other, with oxygen atoms being shared as oxide and hydroxide groups.\n</div>\n",
                "see": [
                  {
                    "title": "bentonite",
                    "link": "https://www.glossary.oilfield.slb.com/en/Terms/b/bentonite.aspx"
                  },
                  {
                    "title": "silica layer",
                    "link": "https://www.glossary.oilfield.slb.com/en/Terms/s/silica_layer.aspx"
                  },
                  {
                    "title": "tetrahedral layer",
                    "link": "https://www.glossary.oilfield.slb.com/en/Terms/t/tetrahedral_layer.aspx"
                  }
                ],
                "more_details": [

                ],
                "synonyms": [

                ],
                "antonyms": [

                ]
              }
            ]
          }
];

Yes, I did. That’s great. Thought you were telling me to refactor everything to match the basic usage.

No sorry mostly just mean you have to be careful that it exports an array and that each “fixture” can be parsed by mirage into a mirage record. You might run into issues with the definitions thing but I think it should still be ok…

1 Like

Wow. Making progress. No more errors! Just pushed the latest into git.

https://github.com/jameshahn2/glossary-app

The only problem now is when I go to http://localhost:4200/words/bean (for example), you see the “HELP” I put in there while frustrated yesterday, but not the word or definition. I’m guessing this is a problem with the router.js? It’s currently set to:

import EmberRouter from "@ember/routing/router";
import config from "./config/environment";

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {
  this.route("home", { path: "/" });
  this.route("words", { path: "/words/:word_id" });
    });

export default Router;

Ah I should have been more specific, I meant the “route handler” code should go in your mirage route handlers (your mirage/config.js), something like this:

this.get('/words', function({ words }, { queryParams }) {
  let { word } = queryParams;
  if (!word) {
    return words.all();
  } else {
    word = word.toLowerCase();
    return words.all().filter(w => w.toLowerCase().includes(word));
  }
});
this.get('/words/:word', function ({ words }, request) {
  return words.findBy({ word: request.params.word });
});

The above is mirage code that creates two GET handlers. They receive the mirage schema and the request as parameters and the schema is how you should interact with the mirage db to filter down what records you need.

Your Ember route should look like this:

import Route from '@ember/routing/route';

export default Route.extend({
  model() {
    return this.store.findRecord('word', params.word_id);
  }
});

Another thing that can make this easier to debug is turning on mirage logging in your mirage scenario (in mirage/scenarios/default.js add a line server.logging = true). This will log all mirage requests/responses in the console so you can see exactly what requests mirage receiving and exactly what it is returning to your app.

1 Like

Thanks for all your help on this @dknutsen. Here is mirage/config.js

export default function() {
  this.namespace = "/api";

this.get('/words', function({ words }, { queryParams }) {
  let { word } = queryParams;
  if (!word) {
    return words.all();
  } else {
    word = word.toLowerCase();
    return words.all().filter(w => w.toLowerCase().includes(word));
  }
});

this.get('/words/:word', function ({ words }, request) {
  return words.findBy({ word: request.params.word });
});
}

The router.js.

import EmberRouter from "@ember/routing/router";
import config from "./config/environment";

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {
  this.route("home", { path: "/" });
  this.route("words", { path: "/words/:word_id" });
    });

export default Router;

And app/pods/words/route.js.

import EmberRouter from "@ember/routing/router";
import config from "./config/environment";

const Router = EmberRouter.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {
  this.route("home", { path: "/" });
  this.route("words", { path: "/words/:word_id" });
    });

export default Router;

Also have this in mirage/scenarios/default.js.

export default function(server) {
server.loadFixtures("words");
server.logging = true
}

With all this in place, now when you go to http://localhost:4200/words/bean you get the following error:

WARNING: You requested a record of type 'word' with id 'bean' but the adapter returned a payload with primary data having an id of '5'. Use 'store.findRecord()' when the requested id is the same as the one returned by the adapter. In other cases use 'store.queryRecord()' instead https://emberjs.com/api/data/classes/DS.Store.html#method_queryRecord

Here is the app/adapters/application.js.

import DS from "ember-data";

export default DS.JSONAPIAdapter.extend({
  namespace: "api"
});

Thoughts?

Also, when you go to http://localhost:4200/words/5 you get:

Error: Assertion Failed: You made a 'findRecord' request for a 'word' with id '5', but the adapter's response did not have any data

Ah yeah I probably should have seen that coming but oh well. Both errors are essentially caused by the same thing… Ember expects each record to have a unique ID, and findRecord is meant to do lookup by id so when the record returned doesn’t match the “id” that you submitted via findRecord it chokes. The second error is caused by the reverse situation, you’re submitting an actual id . but on the backend you’re trying to look up by word instead of id so it’s looking for a word with “word” value of “5” which doesn’t exist.

So… you could use the word as the id since in this case you shouldn’t have duplicates, however in general best practice is to use an abstract id. So your other option is to keep your data how you have it and use query or queryRecord instead of findRecord. In fact if you just change your router map to this:

Router.map(function() {
  this.route("home", { path: "/" });
  this.route("words", { path: "/words/:word" });
});

and your words route to this:

import Route from '@ember/routing/route';

export default Route.extend({
  model() {
    return this.store.queryRecord('word', params.word);
  }
});

I think it should work how you want. Just keep in mind that if you ever do what to look up by word id instead of word.word you’ll need to figure out an unambigious mirage handler setup.

Error while processing route: words Assertion Failed: Expected the primary data returned by the serializer for a `queryRecord` response to be a single object but instead it was an array.

I give up. lol

Oh yeah I’ve never actually used queryrecord now that i think about it but the query should be provided in an object and it will hit the backend as a query, with query params that is. So your mirage config should change to this:

export default function() {
  this.namespace = "/api";

this.get('/words', function({ words }, { queryParams }) {
  let { word } = queryParams;
  if (!word) {
    return words.all();
  } else {
    word = word.toLowerCase();
    return words.findBy({ word });
  }
});
}

And your route code to this:

import Route from '@ember/routing/route';

export default Route.extend({
  model() {
    return this.store.queryRecord('word', { word: params.word });
  }
});

Of course you could also keep your route handler as-is and just use query instead of queryRecord and always treat the model as an array, that would work too.

1 Like

Oh my goodness, I was despairing if this day would ever come…

It freaking works!!

Cannot thank you enough for all of this. For real, for real.

Thank you!!

Awesome! That’s always such a great feeling. Definitely worth playing around with mirage some more, it’s a very powerful library and can really help clarify your API design or how your front-end behaves with minimal effort.

1 Like

Still a lot to learn, but desperately needed a win this week and you made it happen. Cheers!! :beers:

2 Likes

Quick follow-up here. Any reason I’m not getting the expected dasherized url for words/terms with multiple words? Do I need to add dasherize to the words route.js?

works with space between words, but not with dash or run together

Yup you could either dasherize it in the route (but note that you’ll have to un-dasherize it on the mirage side) or you could change the route to use query params instead of a dynamic segment (probably more conventional for a situation like this) and the query param parsing should be handled for you on both sides (ember and mirage).

1 Like