Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ember CLI - RESTAdapter for 3rd party API

I can get data into my app with ic.ajax, but it seems like I should be using the RESTAdapter. The explanations are so simplified, that I'm not sure what to do in various cases. This is what I think should work: (and does with fixtures, a local express server, and http-mocks)

I'm going to use tumblr as the example - since it's always been friendly API in general.

router.js

import Ember from 'ember';
import config from './config/environment';

var Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {

  // tumblr posts
  this.resource('posts', {
    path: '/tumblr'
  });

});

export default Router;

routes/post.js

import Ember from 'ember';

export default Ember.Route.extend({

  model: function() {
    return this.store.find('post');
  }

});

So, as far as I can tell - find() is some magic ajax call... but what if I want to specify jsonp or something?

adapters/post.js

import DS from 'ember-data';

var tumblrBlogName = 'feministlibraryonwheels'; // friends site
var tumblrApiKey = 'UbB4p0GxqNa6wUa8VwpIdqtywjIiA6vljZXyI9wkx9hnQnAFyk';
var tumblrRequestUrl = 'http://api.tumblr.com/v2/blog/' + tumblrBlogName + '.tumblr.com' + '/posts?api_key=' + tumblrApiKey;

export default DS.RESTAdapter.extend({

  host: tumblrRequestUrl

});

This is a little whacky, because the long tumblr endpoint-thing is so squirly - I feel like it should just be http://api.tumblr.com and maybe there is another way to specify the other stuff... or does it just somehow know... very confused... seems like namespace: 'v2' is what that option would be for...

templates/posts.hbs

<section class='container stage'>
<div class='inner-w'>

    <h2>tumblr posts</h2>

    <ul class='block-list event-list'>
        {{#each}}
            <li>
                {{title}}
            </li>

        {{else}}
            No posts are coming up... what's up with that?
        {{/each}}
    </ul>

</div>
</section>

Then this {{#each}} just knows what it's supposed to look for in most cases - but I would like to be explicit.


In all the tutorials I've seen, it's a local server - or http-mocks - and it's just something like this:

import DS from 'ember-data';

export default DS.RESTAdapter.extend({

  host: 'localhoststuff:3000',
  namespace: 'api'

});

Then on top of this - I get what seems like a cors issue GET http://api.tumblr.com/v2/blog/feministlibraryonwheels.tumblr.com/posts?api_key=UbB4p0GxqNa6wUa8VwpIdqtywjIiA6vljZXyI9wkx9hnQnAFyk/posts 401 (Not Authorized)

and it's not like it's really hidden... [http://api.tumblr.com/v2/blog/feministlibraryonwheels.tumblr.com/posts?api_key=UbB4p0GxqNa6wUa8VwpIdqtywjIiA6vljZXyI9wkx9hnQnAFyk][1]

What is the missing link between many quick tutorials - and the RESTAdapter in the real world - with real API's ??? Any direction will be very appreciated.

Also, here is some ic.ajax() attempts I was making as well. The payload gets to the console - but it gets foggy when trying to get the data into the templates

import Ember from 'ember';
import ajax from 'ic-ajax';

export default Ember.Route.extend({

  model: function() {
    var libraryData = ajax({
      url: 'http://www.librarything.com/api_getdata.php?userid=F.L.O.W.&showstructure=1&showTags=1&booksort=title_REV',
      type: 'get',
      dataType: 'jsonp'
    });
    console.log(libraryData);
    return libraryData;
  }

});



EDIT: the 2.4 docs are pretty great. Ember data is stable. You need to know what type of JSON you're getting an if it isn't JSON API format then you need to serialize the data and mold it into that format.

like image 714
sheriffderek Avatar asked Feb 20 '15 07:02

sheriffderek


2 Answers

Using Ember 1.13.3.

With regard to Tumblr specifically, I have the following in adapters/post.js. Hopefully this helps you.

import DS from "ember-data";

export default DS.RESTAdapter.extend({
    host: 'https://api.tumblr.com',
    namespace: 'v2/blog/example.tumblr.com',
    apiKey: 'example',

    ajaxOptions() {
        var hash = this._super.apply(this, arguments);
        hash.data = hash.data || {};
        hash.data.api_key = this.get('apiKey');
        hash.dataType = 'jsonp';
        return hash;
    }
});
like image 68
redjam13 Avatar answered Oct 03 '22 13:10

redjam13


UPDATE for Ember 1.13.0, Ember Data 1.13.0 and above

Tools (Adapters and Serializers)

To connect to an API that isn't Ember-Data friendly (like tumblr) you have to customize an adapter such as the RESTAdapter or JSONAPIAdapter to build the request properly.

(The JSONAPIAdapter is a subclass of RESTAdapter that adjusts a few things for a JSON API such as the Accept: application/vnd.api+json header required by the specs)

Further, if the data retrieved doesn't follow the Ember Data JSON JSON API format, you should customize a serializer such as the JSONSerializer, RESTSerializer or JSONAPISerializer to format and massage the data by selecting the most appropriate serializer to the data returned by your backend. Here's a short summary of these 3 serializers:

JSONSerializer:

  • Base serializer (extends the abstract DS.Serializer)
  • Provides normalization hooks to massage the data received
  • Provides serialization hooks to massage the data sent
  • Expects the usual JSON format

    {
      "id": 1,
      "name": "Bob",
      "friends": [array of friend ids],
      "links": {
        "home": "/bobshome"
      }
    }
    

RESTSerializer:

  • Extends JSONSerializer
  • Expects a similar JSON format (with a "root"):

    {
      "user": {
        "id": 1,
        "name": "Bob",
        "friends": [array of friend ids],
        "links": {
          "home": "/bobshome"
        }
      }
    }
    

JSONAPISerializer

  • Extends JSONSerializer
  • Expects a compliant JSON API payload.

In general, we can mix and match adapters and serializers to whatever fits best our data and URL endpoint structure. Each of these can be applied on a per-application or per-model basis.


Methods

When building the request, depending on the exact behavior you need, you should override appropriate hooks provided by the Ember Data Store (in the Adapter):

Ember Data Find Methods

Each of the hooks above has an appropriate normalize{{HOOK_NAME}}Response method in the serializer (for example normalizeFindRecordResponse) where data can be massaged depending on the way we request it (which hook we call):


Examples

Let's say we want to get Bob's Tumblr blog called "mynameisbob".

Here's a generic example how to do that:

export default DS.RESTAdapter.extend({
    namespace: 'v2/blog/',           // https://guides.emberjs.com/v2.0.0/models/customizing-adapters/#toc_endpoint-path-customization
    host: 'https://api.tumblr.com',  // https://guides.emberjs.com/v2.0.0/models/customizing-adapters/#toc_host-customization
    headers: {                       // https://guides.emberjs.com/v2.0.0/models/customizing-adapters/#toc_headers-customization
        'api_key': 'abcdefg'
    }

    // set any default ajax options you might need
    ajaxOptions(url, type, options = {}) {
        options.crossDomain = true;                   // make it CORS
        options.dataType = 'jsonp';
        return this._super(url, type, options);
    }

    // find a blog
    findRecord: function(store, type, id, snapshot) {
        const URL = this.buildURL(type.modelName, id, snapshot, 'findRecord');
        return this.ajax(URL, 'GET');
    }
});

Usage in a Route:

// ...
model() {
    return this.store.findRecord('blog', 'mynameisbob');
}
// ...

However, there are multiple ways to achieve this. You can also store the api key and the host url as properties of your adapter and just use them to build the URL (using the buildURL hook):

export default DS.RESTAdapter.extend({
    hostUrl: 'https://api.tumblr.com/v2/blog'
    apiKey: 'abcdefg',

    buildURL: function(modelName, id, snapshot, requestType, query) {
        // customize the url based on the parameters
        // lets assume the id is the blog name
        return `${this.get('hostUrl')}/${id}.tumblr.com/posts?api_key=${this.get('apiKey')}`;
    }

    // find a blog
    findRecord: function(store, type, id, snapshot) {
        const URL = this.buildURL(type.modelName, id, snapshot, 'findRecord');
        return this.ajax(URL, 'GET');
    }
});

Here's a Github Repo for a simple Ember app communicating with the Github API

Community driven Adapters to serve as examples:

  • emberobserver.com
  • github.com
  • bower.io

Some useful readings:

  • Fit Any Backend Into Ember
  • Adapter Patterns in Ember

Pre Ember 1.13.0

Here is a simple example on how to write a custom RestAdapter. Essentially, what you need to do is to overwrite the store lookup hooks you need (find, findAll, findQuery ...), as well as buildURL().

Since it's an external API and we need to worry about CORS, we should also override the ajax hook.

Custom Tumblr Adapter:

App.TumblrAdapter = DS.RESTAdapter.extend({

    buildURL: function(type, id, record) {
        var tumblrBlogName = 'feministlibraryonwheels';
        var tumblrApiKey = 'abcdefg';
        var tumblrRequestUrl = 'http://api.tumblr.com/v2/blog/' + tumblrBlogName + '.tumblr.com' + '/posts?api_key=' + tumblrApiKey;
        return tumblrRequestUrl;
    },

    ajax: function(url, method, hash) {
        hash = hash || {};                         // hash may be undefined
        hash.crossDomain = true;                   // make it CORS
        hash.xhrFields = {withCredentials: true};
        return this._super(url, method, hash);
    },

    find: function(store, type, id, record) {
       // customization here or within buildURL
       return this.ajax(this.buildURL(), 'GET');
    },

    findAll: function(store, type, sinceToken) {
        // customization here or within buildURL
        return this.ajax(this.buildURL(), 'GET');
    },

    findQuery: function(store, type, query) {
        // customization here or within buildURL
        return this.ajax(this.buildURL(), 'GET');
    },

    findMany: function(store, type, ids, owner) {
        // customization here or within buildURL
        return this.ajax(this.buildURL(), 'GET');
    }
});

If the response isn't properly formatted for Ember Data, we can fix it quickly in the ajax hook with a simple promise:

ajax: function(url, method, hash) {
    hash = hash || {};                         // hash may be undefined
    hash.crossDomain = true;                   // make it CORS
    hash.xhrFields = {withCredentials: true};
    return this._super(url, method, hash).then(function(json) {
        // Massage data to look like RESTAdapter expects.
        return { tumblrs: [json] };
    });
},

If you wish to properly define all the Models with relationship, you will need to implement a custom RESTSerializer to massage the data properly.

Here is a simple jsbin example I came across on while researching these things myself.

like image 36
nem035 Avatar answered Oct 03 '22 12:10

nem035