Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ember Data Adapter Mixin to use fetch #31

Merged
merged 6 commits into from
Jun 15, 2017

Conversation

nlfurniss
Copy link
Collaborator

I would have liked to do some more testing of the adapter methods, let's discuss how best to do that.

@kratiahuja
Copy link
Collaborator

@nlfurniss how will this work with things like bigpipe?

@nlfurniss
Copy link
Collaborator Author

Yup it works fine with bigpipe (used v-web to test this). The only thing I need to confirm is that the proper tracking headers are attached to requests BPR sends. Have sent Mark and Rajul an email to figure out how to confirm.

@stefanpenner
Copy link
Collaborator

I like this as a stepping stone for ember-data users to use ember-fetch 👍

@@ -28,11 +28,14 @@ export default Ember.Route.extend({
});
```

### Use with Ember Data
To have Ember Data utilize `fetch` instead of jQuery.ajax to make calls to your backend, extend your project's `application` adapter with the `adapter-fetch` mixin.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets give a quick code example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

further docs: https://github.com/github/fetch

### Browser Support

* evergreen / IE9+ / Safari 6.1+ https://github.com/github/fetch#browser-support
* evergreen / IE10+ / Safari 6.1+ https://github.com/github/fetch#browser-support
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah is the polyfil IE10+ only now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, according to their readme: https://github.com/github/fetch#browser-support

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

* @param {Object} options
*/
export function mungOptionsForFetch(options) {
const _options = assign({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit pick, but can we make the param be _options but the one used throughout the function option I think this reads nicer.

throw this.ajaxError(null, response, requestData);
})
.catch((error, response, requestData) => {
throw this.ajaxError(error, response, requestData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As implemented this catch will also catch all errors thrown in the then which precedes it. This includes its own this.ajaxError That seems funky.

I believe simply re-ordering the then and the catch would address this issue.

this._ajaxRequest(hash)
  .catch((error, response, requestData) => { 
    throw.this.ajaxError(error, response, requestData);
  }).then(response => {
    if (response.ok) {
      const bodyPromise = response.json();
      return this.ajaxSuccess(response, bodyPromise, requestData);
    }
    throw this.ajaxError(null, response, requestData);
  });

body,
requestData
);
} catch (error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this try/catch is redundant and can be removed. the caught error will already be caught be the then handler it is being called in.

requestData
);
} catch (e) {
returnedError = e;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any downside to letting this just throw ?

@nlfurniss
Copy link
Collaborator Author

Updated with @stefanpenner's suggestions

@nlfurniss
Copy link
Collaborator Author

so phantom doesn’t have Map or Headers, so my test of the headersToObject helper is failing.

do you want to shim Map/Headers, leave out the test, other?

@nlfurniss nlfurniss force-pushed the master branch 3 times, most recently from 6c87c87 to 9f15853 Compare May 30, 2017 08:14
@tchak
Copy link
Collaborator

tchak commented May 31, 2017

@nlfurniss polyfilled Headers should be available as import { Headers } from 'ember-fetch';

@cibernox
Copy link
Collaborator

@nlfurniss This is fantastic 👏

Counting the seconds to use it.

// GET and HEAD requests can't have a `body`
if (options.data && Object.keys(options.data).length) {
if (options.method === 'GET' || options.method === 'HEAD') {
options.url += `?${serialiazeQueryParams(_options.data)}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one error. This does not serialize nested objects properly.

Example:

serialiazeQueryParams(_options.data) with this implementation generates page=%5Bobject%20Object%5D.

The expected queryParams that is generated by $.ajax is page%5Bnumber%5D=1&page%5Bsize%5D=3

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been playing in the console, this snippet seems to work well for arbitrary nested objects:

function serialiazeQueryParams(queryParamsObject, prefix = null) {
  return Object.keys(queryParamsObject).sort().map((key) => {
    let value = queryParamsObject[key];
    if (prefix) {
      key = `${prefix}[${key}]`;
    }
    if (value !== null && typeof value === 'object') {
      return serialiazeQueryParams(value, key);
    } else {
      return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
    }
  }).join('&');
}

I haven't checked how arrays should work with arrays.

For what is worth, this is the implementation of $.param, which is the thing used by $.ajax: https://github.com/jquery/jquery/blob/master/src/serialize.js#L17-L90

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We spoke offline and this functionality has been added, along with tests

// GET and HEAD requests can't have a `body`
if (options.data && Object.keys(options.data).length) {
if (options.method === 'GET' || options.method === 'HEAD') {
options.url += `?${serialiazeQueryParams(_options.data)}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been playing in the console, this snippet seems to work well for arbitrary nested objects:

function serialiazeQueryParams(queryParamsObject, prefix = null) {
  return Object.keys(queryParamsObject).sort().map((key) => {
    let value = queryParamsObject[key];
    if (prefix) {
      key = `${prefix}[${key}]`;
    }
    if (value !== null && typeof value === 'object') {
      return serialiazeQueryParams(value, key);
    } else {
      return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
    }
  }).join('&');
}

I haven't checked how arrays should work with arrays.

For what is worth, this is the implementation of $.param, which is the thing used by $.ajax: https://github.com/jquery/jquery/blob/master/src/serialize.js#L17-L90

@cibernox
Copy link
Collaborator

cibernox commented Jun 1, 2017

@nlfurniss Why was this closed?

@nlfurniss nlfurniss reopened this Jun 1, 2017
@nlfurniss
Copy link
Collaborator Author

an accident of those history fixes I did :-\

But it's back!

@cibernox
Copy link
Collaborator

cibernox commented Jun 1, 2017

@nlfurniss You scared the s**** out of me ☺️

* @override
*/
_ajaxRequest(options) {
const _options = mungOptionsForFetch(options);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found another issue. This is not respecting the Headers, as in jQuery the options are set using a beforeSend function and in fetch headers must be a Headers object passed on the options.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a PR for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanpenner, how do you feel about adding ED to this addon?

Pull request

})
.then((response) => {
if (response.ok) {
const bodyPromise = response.json();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line also fails if the server responds with a 204 No content, since JSON.parse throws an error trying to parse an empty response.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using the Content-Length header reliable enough? or is 204 an edge case?

let bodyPromise;
if (response.ok && response.status !== 204) {
  bodyPromise = response.json();
} else {
  bodyPromise = {};
}
...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea. I've asked in slack what the right way of handing 204s is.

@cibernox
Copy link
Collaborator

cibernox commented Jun 7, 2017

After some debugging I found that this approach doesn't work if you also run your app in fastboot. I opened this issue in there: ember-fastboot/ember-cli-fastboot#419 (comment)

The tl;dr is that fastboot has an initializer that also replaces the _ajaxRequest method on all adapters to use node-ajax. This mixin and that initializer are essentially fighting.

The solution I see is to put this mixing in a standalone addon, not in here, so that addon can explicitly initialize itself after ember-cli-fastboot and trump over it.

Another approach if the people in ember-cli-fastboot agree would be to move the ember-data compatibility layer elsewhere. Perhaps to ember-data itself, perhaps to an ember-data-fastboot-adapter people has to install.

@stefanpenner
Copy link
Collaborator

The tl;dr is that fastboot has an initializer that also replaces the _ajaxRequest method on all adapters to use node-ajax. This mixin and that initializer are essentially fighting.

We will need to tackle this independent of this issue. I believe fastboot XHR/FETCH interactions needs some love.

var i, len, key;

if (prefix) {
if (isArray(obj)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.isArray

*/
export function serialiazeQueryParams(queryParamsObject) {
var s = [], rbracket = /\[\]$/;
function isArray(obj) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this helper, Array.isArray is available in our target browsers and is also faster.

function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
function add(k, v) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets not redefine this method on each invocation of serialiazeQueryParams, instead hoist it up.

* @returns {String}
*/
export function serialiazeQueryParams(queryParamsObject) {
var s = [], rbracket = /\[\]$/;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rbracket should likely be const RRACKET = /... at the top of the module (no need to redefine it.

@stefanpenner
Copy link
Collaborator

@cibernox you mentioned the fastboot thing, do you feel that is a blocker here?

@cibernox
Copy link
Collaborator

@stefanpenner I'd say that the inability if use ember-fetch (this mixin only, the rest works well) along with fastboot might be a problem for ppl building PWAs without jquery.

TBH, I think that in this case fastboot's approach is a bit intrusive.

@kratiahuja
Copy link
Collaborator

TBH, I think that in this case fastboot's approach is a bit intrusive.

Why is fastboot's approach a bit intrusive? I think the problem is the union with ember-data. If you aren't using ember-data this would work out of the box (even in fastboot). If you chose to use ember-data, then ofcourse you will need to provide a way to override the behavior in fastboot like you are doing here with the mixin.

@stefanpenner
Copy link
Collaborator

stefanpenner commented Jun 14, 2017

Why is fastboot's approach a bit intrusive?

I think fastboot may want to not monkey patch ember-data, but rather provide an XMLHTTPRequest and Fetch global. I think that would be less intrusive, and work with more libraries (pretender/mirage etc). This idea needs more investigation/exploration etc...

@stefanpenner stefanpenner dismissed cibernox’s stale review June 14, 2017 16:44

chatted offline

@stefanpenner stefanpenner merged commit 09e10ef into ember-cli:master Jun 15, 2017
@stefanpenner
Copy link
Collaborator

released as v3.0.0 🎉

issue for fastboot + ED compat: #38

@kratiahuja
Copy link
Collaborator

I think fastboot may want to not monkey patch ember-data, but rather provide an XMLHTTPRequest and Fetch global. I think that would be less intrusive, and work with more libraries (pretender/mirage etc). This idea needs more investigation/exploration etc...

Agreed the monkey patching is bad. but I also think ember-data should be providing an network interface for fastboot or anyone else to define. This way even ember-fetch won't need to define a mixin but just defines the interface for ember data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants