From 55411b2406dad62521423abe4820ec7ff0f8fdfb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Sat, 17 Mar 2018 09:54:28 -0700 Subject: [PATCH 1/4] Add support for ember-fetch --- addon/adapters/json-api.js | 34 +------ addon/adapters/rest.js | 204 +++++++++++++++++++++++++++++-------- package.json | 1 + 3 files changed, 163 insertions(+), 76 deletions(-) diff --git a/addon/adapters/json-api.js b/addon/adapters/json-api.js index 7b5f2e8b1ed..6a7c210feb1 100644 --- a/addon/adapters/json-api.js +++ b/addon/adapters/json-api.js @@ -4,7 +4,6 @@ */ import { dasherize } from '@ember/string'; import RESTAdapter from './rest'; -import { instrument } from 'ember-data/-debug'; import { pluralize } from 'ember-inflector'; /** @@ -155,36 +154,9 @@ const JSONAPIAdapter = RESTAdapter.extend({ @return {Object} */ ajaxOptions(url, type, options) { - let hash = this._super(...arguments); - - if (hash.contentType) { - hash.contentType = 'application/vnd.api+json'; - } - - instrument(function() { - hash.converters = { - 'text json': function(payload) { - let token = heimdall.start('json.parse'); - let json; - try { - json = JSON.parse(payload); - } catch (e) { - json = payload; - } - heimdall.stop(token); - return json; - }, - }; - }); - - let beforeSend = hash.beforeSend; - hash.beforeSend = function(xhr) { - xhr.setRequestHeader('Accept', 'application/vnd.api+json'); - if (beforeSend) { - beforeSend(xhr); - } - }; - + options.contentType = 'application/vnd.api+json'; + let hash = this._super(url, type, options); + hash.headers['accept'] = 'application/vnd.api+json'; return hash; }, diff --git a/addon/adapters/rest.js b/addon/adapters/rest.js index f7267eafa52..9ee3899934d 100644 --- a/addon/adapters/rest.js +++ b/addon/adapters/rest.js @@ -4,13 +4,17 @@ @module ember-data */ -import $ from 'jquery'; +import Ember from 'ember'; +import fetch, { Response } from 'fetch'; +//FIXME: We need to add a direct export of `serializeQueryParams` +import { serializeQueryParams } from 'ember-fetch/mixins/adapter-fetch'; import { Promise as EmberPromise } from 'rsvp'; import { get, computed } from '@ember/object'; import { getOwner } from '@ember/application'; import { run } from '@ember/runloop'; import Adapter from '../adapter'; +import { assign } from '@ember/polyfills'; import { parseResponseHeaders, BuildURLMixin, @@ -24,7 +28,6 @@ import { TimeoutError, AbortError, } from '../-private'; -import { instrument } from 'ember-data/-debug'; import { warn } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; @@ -298,6 +301,11 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { return getOwner(this).lookup('service:fastboot'); }), + useFetch: computed(function() { + let ENV = getOwner(this).resolveRegistration('config:environment'); + return ((ENV && ENV._JQUERY_INTEGRATION) === false) || (Ember.$ === undefined); + }), + /** By default, the RESTAdapter will send the query params sorted alphabetically to the server. @@ -983,16 +991,33 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { let hash = adapter.ajaxOptions(url, type, options); return new Promise(function(resolve, reject) { - hash.success = function(payload, textStatus, jqXHR) { - heimdall.stop(token); - let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); - run.join(null, resolve, response); - }; - hash.error = function(jqXHR, textStatus, errorThrown) { - heimdall.stop(token); - let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); - run.join(null, reject, error); - }; + if (get(this, 'useFetch')) { + hash.success = function(response) { + heimdall.stop(token); + determineBodyPromise(response, requestData).then(payload => { + let response = fetchSuccessHandler(adapter, payload, response, requestData); + run.join(null, resolve, response); + }); + }; + hash.error = function(response, errorThrown) { + heimdall.stop(token); + determineBodyPromise(response, requestData).then(payload => { + let error = fetchErrorHandler(adapter, payload, response, errorThrown, requestData); + run.join(null, reject, error); + }); + }; + } else { + hash.success = function(payload, textStatus, jqXHR) { + heimdall.stop(token); + let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); + run.join(null, resolve, response); + }; + hash.error = function(jqXHR, textStatus, errorThrown) { + heimdall.stop(token); + let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); + run.join(null, reject, error); + }; + } adapter._ajax(hash); }, 'DS: RESTAdapter#ajax ' + type + ' to ' + url); @@ -1004,7 +1029,7 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { @param {Object} options jQuery ajax options to be used for the ajax request */ _ajaxRequest(options) { - $.ajax(options); + Ember.$.ajax(options); }, /** @@ -1022,8 +1047,20 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { } }, + _fetchRequest(options) { + fetch(options.url, options).then(response => { + if (response.ok) { + options.success(response); + } else { + options.error(response); + } + }).catch(error => options.error(new Response(), error)); + }, + _ajax(options) { - if (get(this, 'fastboot.isFastBoot')) { + if (get(this, 'useFetch')) { + this._fetchRequest(options); + } else if (get(this, 'fastboot.isFastBoot')) { this._najaxRequest(options); } else { this._ajaxRequest(options); @@ -1038,43 +1075,33 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { @param {Object} options @return {Object} */ - ajaxOptions(url, type, options) { - let hash = options || {}; - hash.type = type; - hash.dataType = 'json'; - hash.context = this; - - instrument(function() { - hash.converters = { - 'text json': function(payload) { - let token = heimdall.start('json.parse'); - let json; - try { - json = JSON.parse(payload); - } catch (e) { - json = payload; - } - heimdall.stop(token); - return json; - }, - }; - }); - - if (hash.data && type !== 'GET') { - hash.contentType = 'application/json; charset=utf-8'; - hash.data = JSON.stringify(hash.data); - } + ajaxOptions(url, method, options) { + options = assign({ + url, + method, + type: method + }, options); let headers = get(this, 'headers'); if (headers !== undefined) { - hash.beforeSend = function(xhr) { - Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key])); - }; + options.headers = assign({}, options.headers, headers); + } else if (!options.headers) { + options.headers = {}; + } + if (options.data && options.type !== 'GET') { + let contentType = options.contentType || 'application/json; charset=utf-8'; + options.headers['content-type'] = contentType; + } + + if (get(this, 'useFetch')) { + options = fetchOptions(options, this); + } else { + options = ajaxOptions(options, this); } - hash.url = this._ajaxURL(url); + options.url = this._ajaxURL(options.url); - return hash; + return options; }, _ajaxURL(url) { @@ -1262,6 +1289,17 @@ function endsWith(string, suffix) { } } +function fetchSuccessHandler(adapter, payload, response, requestData) { + let responseData = fetchResponseData(response); + return ajaxSuccess(adapter, payload, requestData, responseData); +} + +function fetchErrorHandler(adapter, payload, response, errorThrown, requestData) { + let responseData = fetchResponseData(response); + responseData.errorThrown = errorThrown; + return ajaxError(adapter, payload, requestData, responseData); +} + function ajaxSuccessHandler(adapter, payload, jqXHR, requestData) { let responseData = ajaxResponseData(jqXHR); return ajaxSuccess(adapter, payload, requestData, responseData); @@ -1274,6 +1312,14 @@ function ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData) { return ajaxError(adapter, payload, requestData, responseData); } +function fetchResponseData(response) { + return { + status: response.status, + textStatus: response.textStatus, + headers: headersToObject(response.headers) + }; +} + function ajaxResponseData(jqXHR) { return { status: jqXHR.status, @@ -1282,4 +1328,72 @@ function ajaxResponseData(jqXHR) { }; } +function determineBodyPromise(response, requestData) { + return response.text().then(function(payload) { + try { + payload = JSON.parse(payload); + } catch (error) { + if (!(error instanceof SyntaxError)) { + throw error; + } + const status = response.status; + if (response.ok && (status === 204 || status === 205 || requestData.method === 'HEAD')) { + payload = { data: null }; + } else { + warn('This response was unable to be parsed as json.', payload); + } + } + return payload; + }); +} + +function headersToObject(headers) { + let headersObject = {}; + + if (headers) { + headers.forEach((value, key) => headersObject[key] = value); + } + + return headersObject; +} + +/** + * Helper function that translates the options passed to `jQuery.ajax` into a format that `fetch` expects. + * @param {Object} _options + * @param {DS.Adapter} adapter + * @returns {Object} + */ +export function fetchOptions(options, adapter) { + options.credentials = 'same-origin'; + + if (options.data) { + // GET and HEAD requests can't have a `body` + if ((options.method === 'GET' || options.method === 'HEAD')) { + // If no options are passed, Ember Data sets `data` to an empty object, which we test for. + if (Object.keys(options.data).length) { + // Test if there are already query params in the url (mimics jQuey.ajax). + const queryParamDelimiter = options.url.indexOf('?') > -1 ? '&' : '?'; + options.url += `${queryParamDelimiter}${serializeQueryParams(options.data)}`; + } + } else { + // NOTE: a request's body cannot be an object, so we stringify it if it is. + // JSON.stringify removes keys with values of `undefined` (mimics jQuery.ajax). + options.body = JSON.stringify(options.data); + } + } + + return options; +} + +function ajaxOptions(options, adapter) { + options.dataType = 'json'; + options.context = adapter; + + if (options.data && options.type !== 'GET') { + options.data = JSON.stringify(options.data); + } + + return options; +} + export default RESTAdapter; diff --git a/package.json b/package.json index 109a86c5638..20d9e85ffbb 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "ember-cli-test-info": "^1.0.0", "ember-cli-typescript": "^2.0.0-beta.2", "ember-cli-version-checker": "^3.1.2", + "ember-fetch": "^5.1.3", "ember-inflector": "^3.0.0", "git-repo-info": "^2.0.0", "heimdalljs": "^0.3.0", From 6216c564d015586547fb04a966b1869a7bbf57ad Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Sat, 8 Sep 2018 20:29:09 -0400 Subject: [PATCH 2/4] fix some tests move ember-fetch to a peer-dependency fix jsonapi adapter tests add tests for fetch --- addon/adapters/json-api.js | 2 +- addon/adapters/rest.js | 101 +++++++------ config/ember-try.js | 24 ++++ package.json | 5 +- .../adapters/json-api-adapter/ajax-test.js | 15 +- .../adapters/json-api-adapter/fetch-test.js | 75 ++++++++++ tests/unit/adapters/rest-adapter/ajax-test.js | 11 ++ .../unit/adapters/rest-adapter/fetch-test.js | 135 ++++++++++++++++++ 8 files changed, 321 insertions(+), 47 deletions(-) create mode 100644 tests/unit/adapters/json-api-adapter/fetch-test.js create mode 100644 tests/unit/adapters/rest-adapter/fetch-test.js diff --git a/addon/adapters/json-api.js b/addon/adapters/json-api.js index 6a7c210feb1..152972ff259 100644 --- a/addon/adapters/json-api.js +++ b/addon/adapters/json-api.js @@ -156,7 +156,7 @@ const JSONAPIAdapter = RESTAdapter.extend({ ajaxOptions(url, type, options) { options.contentType = 'application/vnd.api+json'; let hash = this._super(url, type, options); - hash.headers['accept'] = 'application/vnd.api+json'; + hash.headers['Accept'] = 'application/vnd.api+json'; return hash; }, diff --git a/addon/adapters/rest.js b/addon/adapters/rest.js index 9ee3899934d..b1a7de96397 100644 --- a/addon/adapters/rest.js +++ b/addon/adapters/rest.js @@ -9,7 +9,7 @@ import fetch, { Response } from 'fetch'; //FIXME: We need to add a direct export of `serializeQueryParams` import { serializeQueryParams } from 'ember-fetch/mixins/adapter-fetch'; -import { Promise as EmberPromise } from 'rsvp'; +import RSVP, { Promise as EmberPromise } from 'rsvp'; import { get, computed } from '@ember/object'; import { getOwner } from '@ember/application'; import { run } from '@ember/runloop'; @@ -303,7 +303,7 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { useFetch: computed(function() { let ENV = getOwner(this).resolveRegistration('config:environment'); - return ((ENV && ENV._JQUERY_INTEGRATION) === false) || (Ember.$ === undefined); + return (ENV && ENV._JQUERY_INTEGRATION) === false || Ember.$ === undefined; }), /** @@ -990,34 +990,40 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { }; let hash = adapter.ajaxOptions(url, type, options); - return new Promise(function(resolve, reject) { - if (get(this, 'useFetch')) { - hash.success = function(response) { + if (get(this, 'useFetch')) { + return this._fetchRequest(hash) + .catch(() => { heimdall.stop(token); - determineBodyPromise(response, requestData).then(payload => { - let response = fetchSuccessHandler(adapter, payload, response, requestData); - run.join(null, resolve, response); - }); - }; - hash.error = function(response, errorThrown) { + }) + .then(response => { heimdall.stop(token); - determineBodyPromise(response, requestData).then(payload => { - let error = fetchErrorHandler(adapter, payload, response, errorThrown, requestData); - run.join(null, reject, error); + + return RSVP.hash({ + response, + payload: determineBodyPromise(response, requestData), }); - }; - } else { - hash.success = function(payload, textStatus, jqXHR) { - heimdall.stop(token); - let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); - run.join(null, resolve, response); - }; - hash.error = function(jqXHR, textStatus, errorThrown) { - heimdall.stop(token); - let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); - run.join(null, reject, error); - }; - } + }) + .then(({ response, payload }) => { + if (response.ok) { + return fetchSuccessHandler(adapter, payload, response, requestData); + } else { + throw fetchErrorHandler(adapter, payload, response, null, requestData); + } + }); + } + + return new Promise(function(resolve, reject) { + hash.success = function(payload, textStatus, jqXHR) { + heimdall.stop(token); + let response = ajaxSuccessHandler(adapter, payload, jqXHR, requestData); + run.join(null, resolve, response); + }; + + hash.error = function(jqXHR, textStatus, errorThrown) { + heimdall.stop(token); + let error = ajaxErrorHandler(adapter, jqXHR, errorThrown, requestData); + run.join(null, reject, error); + }; adapter._ajax(hash); }, 'DS: RESTAdapter#ajax ' + type + ' to ' + url); @@ -1048,13 +1054,15 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { }, _fetchRequest(options) { - fetch(options.url, options).then(response => { - if (response.ok) { - options.success(response); - } else { - options.error(response); - } - }).catch(error => options.error(new Response(), error)); + fetch(options.url, options) + .then(response => { + if (response.ok) { + options.success(response); + } else { + options.error(response); + } + }) + .catch(error => options.error(new Response(), error)); }, _ajax(options) { @@ -1076,11 +1084,14 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { @return {Object} */ ajaxOptions(url, method, options) { - options = assign({ - url, - method, - type: method - }, options); + options = assign( + { + url, + method, + type: method, + }, + options + ); let headers = get(this, 'headers'); if (headers !== undefined) { @@ -1088,6 +1099,7 @@ const RESTAdapter = Adapter.extend(BuildURLMixin, { } else if (!options.headers) { options.headers = {}; } + if (options.data && options.type !== 'GET') { let contentType = options.contentType || 'application/json; charset=utf-8'; options.headers['content-type'] = contentType; @@ -1316,7 +1328,7 @@ function fetchResponseData(response) { return { status: response.status, textStatus: response.textStatus, - headers: headersToObject(response.headers) + headers: headersToObject(response.headers), }; } @@ -1351,7 +1363,7 @@ function headersToObject(headers) { let headersObject = {}; if (headers) { - headers.forEach((value, key) => headersObject[key] = value); + headers.forEach((value, key) => (headersObject[key] = value)); } return headersObject; @@ -1368,7 +1380,7 @@ export function fetchOptions(options, adapter) { if (options.data) { // GET and HEAD requests can't have a `body` - if ((options.method === 'GET' || options.method === 'HEAD')) { + if (options.method === 'GET' || options.method === 'HEAD') { // If no options are passed, Ember Data sets `data` to an empty object, which we test for. if (Object.keys(options.data).length) { // Test if there are already query params in the url (mimics jQuey.ajax). @@ -1391,8 +1403,13 @@ function ajaxOptions(options, adapter) { if (options.data && options.type !== 'GET') { options.data = JSON.stringify(options.data); + options.contentType = 'application/json; charset=utf-8'; } + options.beforeSend = function(xhr) { + Object.keys(options.headers).forEach(key => xhr.setRequestHeader(key, options.headers[key])); + }; + return options; } diff --git a/config/ember-try.js b/config/ember-try.js index 63aec9222f2..f1f55b8488d 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -16,6 +16,18 @@ module.exports = function() { bower: {}, npm: {}, }, + { + name: 'ember-lts-2.18', + env: { + EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), + }, + npm: { + devDependencies: { + '@ember/jquery': '^0.5.1', + 'ember-source': '~2.18.0', + }, + }, + }, { name: 'ember-lts-2.18', npm: { @@ -24,6 +36,18 @@ module.exports = function() { }, }, }, + { + name: 'ember-lts-3.4', + env: { + EMBER_OPTIONAL_FEATURES: JSON.stringify({ 'jquery-integration': true }), + }, + npm: { + devDependencies: { + '@ember/jquery': '^0.5.1', + 'ember-source': '~3.4.0', + }, + }, + }, { name: 'ember-lts-3.4', npm: { diff --git a/package.json b/package.json index 20d9e85ffbb..7996b5dce2d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "ember-cli-test-info": "^1.0.0", "ember-cli-typescript": "^2.0.0-beta.2", "ember-cli-version-checker": "^3.1.2", - "ember-fetch": "^5.1.3", "ember-inflector": "^3.0.0", "git-repo-info": "^2.0.0", "heimdalljs": "^0.3.0", @@ -63,6 +62,9 @@ "semver": "^5.6.0", "silent-error": "^1.1.1" }, + "peerDependencies": { + "ember-fetch": "^5.1.3" + }, "devDependencies": { "@babel/plugin-transform-typescript": "^7.2.0", "@ember-decorators/babel-transforms": "^5.1.4", @@ -103,6 +105,7 @@ "ember-disable-prototype-extensions": "^1.1.3", "ember-export-application-global": "^2.0.0", "ember-load-initializers": "^2.0.0", + "ember-fetch": "^5.1.3", "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^4.4.1", "ember-qunit-assert-helpers": "^0.2.1", diff --git a/tests/unit/adapters/json-api-adapter/ajax-test.js b/tests/unit/adapters/json-api-adapter/ajax-test.js index 8d39cf67ea7..6f497553b52 100644 --- a/tests/unit/adapters/json-api-adapter/ajax-test.js +++ b/tests/unit/adapters/json-api-adapter/ajax-test.js @@ -7,6 +7,10 @@ import DS from 'ember-data'; let Person, Place, store, adapter, env; +function alphabetize(headers) { + return headers.sort((a, b) => (a[0] > b[0] ? 1 : -1)); +} + module('unit/adapters/json-api-adapter/ajax - building requests', { beforeEach() { Person = { modelName: 'person' }; @@ -35,7 +39,11 @@ test('ajaxOptions() adds Accept when no other headers exist', function(assert) { }, }; ajaxOptions.beforeSend(fakeXHR); - assert.deepEqual(receivedHeaders, [['Accept', 'application/vnd.api+json']], 'headers assigned'); + assert.deepEqual( + alphabetize(receivedHeaders), + [['Accept', 'application/vnd.api+json']], + 'headers assigned' + ); }); test('ajaxOptions() adds Accept header to existing headers', function(assert) { @@ -51,7 +59,7 @@ test('ajaxOptions() adds Accept header to existing headers', function(assert) { }; ajaxOptions.beforeSend(fakeXHR); assert.deepEqual( - receivedHeaders, + alphabetize(receivedHeaders), [['Accept', 'application/vnd.api+json'], ['Other-key', 'Other Value']], 'headers assigned' ); @@ -69,8 +77,9 @@ test('ajaxOptions() adds Accept header to existing computed properties headers', }, }; ajaxOptions.beforeSend(fakeXHR); + assert.deepEqual( - receivedHeaders, + alphabetize(receivedHeaders), [['Accept', 'application/vnd.api+json'], ['Other-key', 'Other Value']], 'headers assigned' ); diff --git a/tests/unit/adapters/json-api-adapter/fetch-test.js b/tests/unit/adapters/json-api-adapter/fetch-test.js new file mode 100644 index 00000000000..01ae37e23a3 --- /dev/null +++ b/tests/unit/adapters/json-api-adapter/fetch-test.js @@ -0,0 +1,75 @@ +import { run } from '@ember/runloop'; +import setupStore from 'dummy/tests/helpers/store'; + +import { module, test } from 'qunit'; + +import DS from 'ember-data'; + +let Person, Place, store, adapter, env; + +module('unit/adapters/json-api-adapter/fetch - building requests', { + beforeEach() { + Person = { modelName: 'person' }; + Place = { modelName: 'place' }; + env = setupStore({ adapter: DS.JSONAPIAdapter, person: Person, place: Place }); + store = env.store; + adapter = env.adapter; + adapter.set('useFetch', true); + }, + + afterEach() { + run(() => { + store.destroy(); + env.container.destroy(); + }); + }, +}); + +test('ajaxOptions() adds Accept when no other headers exist', function(assert) { + let url = 'example.com'; + let type = 'GET'; + let ajaxOptions = adapter.ajaxOptions(url, type, {}); + let receivedHeaders = ajaxOptions.headers; + + assert.deepEqual( + receivedHeaders, + { + Accept: 'application/vnd.api+json', + }, + 'headers assigned' + ); +}); + +test('ajaxOptions() adds Accept header to existing headers', function(assert) { + adapter.headers = { 'Other-key': 'Other Value' }; + let url = 'example.com'; + let type = 'GET'; + let ajaxOptions = adapter.ajaxOptions(url, type, {}); + let receivedHeaders = ajaxOptions.headers; + + assert.deepEqual( + receivedHeaders, + { + Accept: 'application/vnd.api+json', + 'Other-key': 'Other Value', + }, + 'headers assigned' + ); +}); + +test('ajaxOptions() adds Accept header to existing computed properties headers', function(assert) { + adapter.headers = { 'Other-key': 'Other Value' }; + let url = 'example.com'; + let type = 'GET'; + let ajaxOptions = adapter.ajaxOptions(url, type, {}); + let receivedHeaders = ajaxOptions.headers; + + assert.deepEqual( + receivedHeaders, + { + Accept: 'application/vnd.api+json', + 'Other-key': 'Other Value', + }, + 'headers assigned' + ); +}); diff --git a/tests/unit/adapters/rest-adapter/ajax-test.js b/tests/unit/adapters/rest-adapter/ajax-test.js index f8f82b9e53d..2581b635a0c 100644 --- a/tests/unit/adapters/rest-adapter/ajax-test.js +++ b/tests/unit/adapters/rest-adapter/ajax-test.js @@ -87,6 +87,7 @@ test('ajaxOptions() do not serializes data when GET', function(assert) { let url = 'example.com'; let type = 'GET'; let ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); + delete ajaxOptions.beforeSend; assert.deepEqual(ajaxOptions, { context: adapter, @@ -95,6 +96,8 @@ test('ajaxOptions() do not serializes data when GET', function(assert) { }, dataType: 'json', type: 'GET', + method: 'GET', + headers: {}, url: 'example.com', }); }); @@ -103,6 +106,7 @@ test('ajaxOptions() serializes data when not GET', function(assert) { let url = 'example.com'; let type = 'POST'; let ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); + delete ajaxOptions.beforeSend; assert.deepEqual(ajaxOptions, { contentType: 'application/json; charset=utf-8', @@ -110,6 +114,10 @@ test('ajaxOptions() serializes data when not GET', function(assert) { data: '{"key":"value"}', dataType: 'json', type: 'POST', + method: 'POST', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, url: 'example.com', }); }); @@ -118,11 +126,14 @@ test('ajaxOptions() empty data', function(assert) { let url = 'example.com'; let type = 'POST'; let ajaxOptions = adapter.ajaxOptions(url, type, {}); + delete ajaxOptions.beforeSend; assert.deepEqual(ajaxOptions, { context: adapter, dataType: 'json', type: 'POST', + method: 'POST', + headers: {}, url: 'example.com', }); }); diff --git a/tests/unit/adapters/rest-adapter/fetch-test.js b/tests/unit/adapters/rest-adapter/fetch-test.js new file mode 100644 index 00000000000..4e11f58772e --- /dev/null +++ b/tests/unit/adapters/rest-adapter/fetch-test.js @@ -0,0 +1,135 @@ +import { resolve, Promise as EmberPromise } from 'rsvp'; +import { run } from '@ember/runloop'; +import setupStore from 'dummy/tests/helpers/store'; + +import { module, test } from 'qunit'; + +import DS from 'ember-data'; + +var Person, Place, store, adapter, env; + +module('unit/adapters/rest-adapter/fetch - building requests', { + beforeEach() { + Person = { modelName: 'person' }; + Place = { modelName: 'place' }; + env = setupStore({ adapter: DS.RESTAdapter, person: Person, place: Place }); + store = env.store; + adapter = env.adapter; + adapter.set('useFetch', true); + }, + + afterEach() { + run(() => { + store.destroy(); + env.container.destroy(); + }); + }, +}); + +test('When an id is searched, the correct url should be generated', function(assert) { + assert.expect(2); + + let count = 0; + + adapter.ajax = function(url, method) { + if (count === 0) { + assert.equal(url, '/people/1', 'should create the correct url'); + } + if (count === 1) { + assert.equal(url, '/places/1', 'should create the correct url'); + } + count++; + return resolve(); + }; + + return run(() => { + return EmberPromise.all([ + adapter.findRecord(store, Person, 1, {}), + adapter.findRecord(store, Place, 1, {}), + ]); + }); +}); + +test(`id's should be sanatized`, function(assert) { + assert.expect(1); + + adapter.ajax = function(url, method) { + assert.equal(url, '/people/..%2Fplace%2F1', 'should create the correct url'); + return resolve(); + }; + + return run(() => adapter.findRecord(store, Person, '../place/1', {})); +}); + +test('ajaxOptions() headers are set', function(assert) { + adapter.headers = { + 'Content-Type': 'application/json', + 'Other-key': 'Other Value', + }; + + let url = 'example.com'; + let type = 'GET'; + let ajaxOptions = adapter.ajaxOptions(url, type, {}); + let receivedHeaders = ajaxOptions.headers; + + assert.deepEqual( + receivedHeaders, + { + 'Content-Type': 'application/json', + 'Other-key': 'Other Value', + }, + 'headers assigned' + ); +}); + +test('ajaxOptions() do not serializes data when GET', function(assert) { + let url = 'example.com'; + let type = 'GET'; + let ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); + delete ajaxOptions.beforeSend; + + assert.deepEqual(ajaxOptions, { + credentials: 'same-origin', + data: { + key: 'value', + }, + type: 'GET', + method: 'GET', + headers: {}, + url: 'example.com?key=value', + }); +}); + +test('ajaxOptions() serializes data when not GET', function(assert) { + let url = 'example.com'; + let type = 'POST'; + let ajaxOptions = adapter.ajaxOptions(url, type, { data: { key: 'value' } }); + delete ajaxOptions.beforeSend; + + assert.deepEqual(ajaxOptions, { + credentials: 'same-origin', + data: { key: 'value' }, + body: '{"key":"value"}', + type: 'POST', + method: 'POST', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + url: 'example.com', + }); +}); + +test('ajaxOptions() empty data', function(assert) { + let url = 'example.com'; + let type = 'POST'; + let ajaxOptions = adapter.ajaxOptions(url, type, {}); + delete ajaxOptions.beforeSend; + + assert.deepEqual(ajaxOptions, { + credentials: 'same-origin', + type: 'POST', + method: 'POST', + headers: {}, + url: 'example.com', + }); +}); From 0e8c5bde93f5ac3e826eec9c924fa444a7fd6ffd Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 3 Dec 2018 13:07:39 +0100 Subject: [PATCH 3/4] Update ember-fetch --- addon/adapters/rest.js | 23 ++--------------------- package.json | 4 ++-- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/addon/adapters/rest.js b/addon/adapters/rest.js index b1a7de96397..ab0ea8766d9 100644 --- a/addon/adapters/rest.js +++ b/addon/adapters/rest.js @@ -6,8 +6,8 @@ import Ember from 'ember'; import fetch, { Response } from 'fetch'; -//FIXME: We need to add a direct export of `serializeQueryParams` -import { serializeQueryParams } from 'ember-fetch/mixins/adapter-fetch'; +import serializeQueryParams from 'ember-fetch/utils/serialize-query-params'; +import determineBodyPromise from 'ember-fetch/utils/determine-body-promise'; import RSVP, { Promise as EmberPromise } from 'rsvp'; import { get, computed } from '@ember/object'; @@ -1340,25 +1340,6 @@ function ajaxResponseData(jqXHR) { }; } -function determineBodyPromise(response, requestData) { - return response.text().then(function(payload) { - try { - payload = JSON.parse(payload); - } catch (error) { - if (!(error instanceof SyntaxError)) { - throw error; - } - const status = response.status; - if (response.ok && (status === 204 || status === 205 || requestData.method === 'HEAD')) { - payload = { data: null }; - } else { - warn('This response was unable to be parsed as json.', payload); - } - } - return payload; - }); -} - function headersToObject(headers) { let headersObject = {}; diff --git a/package.json b/package.json index 7996b5dce2d..15625a2e66d 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "silent-error": "^1.1.1" }, "peerDependencies": { - "ember-fetch": "^5.1.3" + "ember-fetch": "^6.2.2" }, "devDependencies": { "@babel/plugin-transform-typescript": "^7.2.0", @@ -105,7 +105,7 @@ "ember-disable-prototype-extensions": "^1.1.3", "ember-export-application-global": "^2.0.0", "ember-load-initializers": "^2.0.0", - "ember-fetch": "^5.1.3", + "ember-fetch": "^6.2.2", "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^4.4.1", "ember-qunit-assert-helpers": "^0.2.1", From e7f7b87fdfa5a6a4d4db73a05e80e98963cfac65 Mon Sep 17 00:00:00 2001 From: Cyrille David Date: Sat, 9 Mar 2019 12:47:34 +0100 Subject: [PATCH 4/4] Fix tests execution (#1) * Fix lint error * Fix `_requireBuildPackages` error * Reset yarn.lock from master There seems to be some issues in the yarn.lock To solve them I ran: - `git checkout master yarn.lock` - `yarn` Throw an error in case of dependency mismatch (#2) * Throw an error in case of dependency mismatch An app (or addon) that consumes ember-data should have: - either jquery - or ember-fetch version 6.0.0 or above Throw a build error if it's not the case. * Remove ember-fetch as a peerDependency Move ember-fetch to dependencies (#3) Indeed, two `ember-fetch`'s modules are imported in rest-adapter. --- addon/adapters/json-api.js | 1 - package.json | 7 +--- yarn.lock | 76 +++++++++++++++++++++++++++++--------- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/addon/adapters/json-api.js b/addon/adapters/json-api.js index 152972ff259..14348b3f20a 100644 --- a/addon/adapters/json-api.js +++ b/addon/adapters/json-api.js @@ -1,4 +1,3 @@ -/* global heimdall */ /** @module ember-data */ diff --git a/package.json b/package.json index 15625a2e66d..4bc6bb93e42 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "ember-cli-test-info": "^1.0.0", "ember-cli-typescript": "^2.0.0-beta.2", "ember-cli-version-checker": "^3.1.2", + "ember-fetch": "^6.2.2", "ember-inflector": "^3.0.0", "git-repo-info": "^2.0.0", "heimdalljs": "^0.3.0", @@ -62,9 +63,6 @@ "semver": "^5.6.0", "silent-error": "^1.1.1" }, - "peerDependencies": { - "ember-fetch": "^6.2.2" - }, "devDependencies": { "@babel/plugin-transform-typescript": "^7.2.0", "@ember-decorators/babel-transforms": "^5.1.4", @@ -105,10 +103,9 @@ "ember-disable-prototype-extensions": "^1.1.3", "ember-export-application-global": "^2.0.0", "ember-load-initializers": "^2.0.0", - "ember-fetch": "^6.2.2", "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^4.4.1", - "ember-qunit-assert-helpers": "^0.2.1", + "ember-qunit-assert-helpers": "^0.2.2", "ember-resolver": "^5.0.1", "ember-source": "~3.8.0", "ember-source-channel-url": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index eec7ecae820..c547f450f65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,11 +982,6 @@ resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@^11.10.5": - version "11.11.0" - resolved "https://registry.npmjs.org/@types/node/-/node-11.11.0.tgz#070e9ce7c90e727aca0e0c14e470f9a93ffe9390" - integrity sha512-D5Rt+HXgEywr3RQJcGlZUCTCx1qVbCZpVk3/tOOA6spLNZdGm8BU+zRgdRYDoF1pO3RuXLxADzMrF903JlQXqg== - "@types/node@^9.6.0": version "9.6.45" resolved "https://registry.npmjs.org/@types/node/-/node-9.6.45.tgz#a9e5cfd026a3abaaf17e3c0318a470da9f2f178e" @@ -1022,7 +1017,7 @@ abbrev@1: resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abortcontroller-polyfill@^1.1.9: +abortcontroller-polyfill@^1.1.9, abortcontroller-polyfill@^1.2.1: version "1.2.6" resolved "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.2.6.tgz#e8cc55ca43dd269723ac02d8d9dd2f34e1c3e229" integrity sha512-BP/ixRR9LiVr83IItbONoebJK5nRcHVSHasftOTT8v9VVP9xDud1rmvMwd5KXaKL1LG5sIsX6tUpIZcYYdGJlg== @@ -2173,7 +2168,7 @@ broccoli-clean-css@^1.1.0: inline-source-map-comment "^1.0.5" json-stable-stringify "^1.0.0" -broccoli-concat@^3.7.3: +broccoli-concat@^3.2.2, broccoli-concat@^3.7.3: version "3.7.3" resolved "https://registry.npmjs.org/broccoli-concat/-/broccoli-concat-3.7.3.tgz#0dca01311567ffb13180e6b4eb111824628e4885" integrity sha512-2Ma9h81EJ0PRb9n4sW0i8KZlcnpTQfKxcj87zvi5DFe1fd8CTDEdseHDotK2beuA2l+LbgVPfd8EHaBJKm/Y8g== @@ -2499,6 +2494,17 @@ broccoli-string-replace@^0.1.2: broccoli-persistent-filter "^1.1.5" minimatch "^3.0.3" +broccoli-templater@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/broccoli-templater/-/broccoli-templater-2.0.2.tgz#285a892071c0b3ad5ebc275d9e8b3465e2d120d6" + integrity sha512-71KpNkc7WmbEokTQpGcbGzZjUIY1NSVa3GB++KFKAfx5SZPUozCOsBlSTwxcv8TLoCAqbBnsX5AQPgg6vJ2l9g== + dependencies: + broccoli-plugin "^1.3.1" + fs-tree-diff "^0.5.9" + lodash.template "^4.4.0" + rimraf "^2.6.2" + walk-sync "^0.3.3" + broccoli-test-helper@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/broccoli-test-helper/-/broccoli-test-helper-2.0.0.tgz#1cfbb76f7e856ad8df96d55ee2f5e0dddddf5d4f" @@ -2583,7 +2589,7 @@ browserslist@^3.2.6: caniuse-lite "^1.0.30000844" electron-to-chromium "^1.3.47" -browserslist@^4.3.4: +browserslist@^4.0.0, browserslist@^4.3.4: version "4.4.2" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2" integrity sha512-ISS/AIAiHERJ3d45Fz0AVYKkgcy+F/eJHzKEvv1j0wwKGKD9T3BrwKr/5g45L+Y4XIK5PlTqefHciRFcfE1Jxg== @@ -2694,7 +2700,17 @@ can-symlink@^1.0.0: dependencies: tmp "0.0.28" -caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000939: +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000939: version "1.0.30000942" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000942.tgz#454139b28274bce70bfe1d50c30970df7430c6e4" integrity sha512-wLf+IhZUy2rfz48tc40OH7jHjXjnvDFEYqBHluINs/6MgzoNLPf25zhE4NOVzqxLKndf+hau81sAW0RcGHIaBQ== @@ -3384,7 +3400,7 @@ ember-cli-babel-plugin-helpers@^1.0.0: resolved "https://registry.npmjs.org/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.0.2.tgz#d4bec0f32febc530e621ea8d66d3365727cb5e6c" integrity sha512-tTWmHiIvadgtu0i+Zlb5Jnue69qO6dtACcddkRhhV+m9NfAr+2XNoTKRSeGL8QyRDhfWeo4rsK9dqPrU4PQ+8g== -ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.12.0, ember-cli-babel@^6.16.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.8.1, ember-cli-babel@^6.9.0: +ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.12.0, ember-cli-babel@^6.16.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.8.1, ember-cli-babel@^6.8.2, ember-cli-babel@^6.9.0: version "6.18.0" resolved "https://registry.npmjs.org/ember-cli-babel/-/ember-cli-babel-6.18.0.tgz#3f6435fd275172edeff2b634ee7b29ce74318957" integrity sha512-7ceC8joNYxY2wES16iIBlbPSxwKDBhYwC8drU3ZEvuPDMwVv1KzxCNu1fvxyFEBWhwaRNTUxSCsEVoTd9nosGA== @@ -3809,6 +3825,24 @@ ember-export-application-global@^2.0.0: dependencies: ember-cli-babel "^6.0.0-beta.7" +ember-fetch@^6.2.2: + version "6.4.0" + resolved "https://registry.npmjs.org/ember-fetch/-/ember-fetch-6.4.0.tgz#806b2acfbda07f3b0dd48a9fcdbc575c5519611d" + integrity sha512-/GtJWQiUAAOX2HMGuLrWQMNWO9YRNH9+AiMzZYc5PrTHKdv+Ib5fciysz0+e97Dj9vyvF2mQa49mrosAPW9ziQ== + dependencies: + abortcontroller-polyfill "^1.2.1" + broccoli-concat "^3.2.2" + broccoli-debug "^0.6.5" + broccoli-merge-trees "^3.0.0" + broccoli-rollup "^2.1.1" + broccoli-stew "^2.0.0" + broccoli-templater "^2.0.1" + calculate-cache-key-for-tree "^1.1.0" + caniuse-api "^3.0.0" + ember-cli-babel "^6.8.2" + node-fetch "^2.3.0" + whatwg-fetch "^3.0.0" + ember-inflector@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/ember-inflector/-/ember-inflector-3.0.0.tgz#7e1ee8aaa0fa773ba0905d8b7c0786354d890ee1" @@ -3833,7 +3867,7 @@ ember-maybe-import-regenerator@^0.1.6: ember-cli-babel "^6.0.0-beta.4" regenerator-runtime "^0.9.5" -ember-qunit-assert-helpers@^0.2.1: +ember-qunit-assert-helpers@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/ember-qunit-assert-helpers/-/ember-qunit-assert-helpers-0.2.2.tgz#6fec8a33fd0d2c3fb6202f849291a309581727a4" integrity sha512-P5eAqD753+p/qEeBi6OGpl2EzRxx8O9dUnr6HgyxU9fqQsSNQkJNGZ+ajbtePI8oMDGm+X7uOnf1+BgQ7eJ7qg== @@ -4758,11 +4792,9 @@ get-caller-file@^1.0.1: integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== get-caller-file@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.4.tgz#b096b4a108afa0a955d62e0572ef3e9fa6c729b9" - integrity sha512-asZ7znBEyqkVHAjMI14GLeskxIJjn1Gplsvh1neaiHXgqRasKDR4uFnkMQXLMb7+0wn221u5vdY6QBkhgxkx0Q== - dependencies: - "@types/node" "^11.10.5" + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.0: version "2.0.0" @@ -6150,6 +6182,11 @@ lodash.keys@~2.3.0: lodash._shimkeys "~2.3.0" lodash.isobject "~2.3.0" +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.merge@^4.3.1, lodash.merge@^4.6.0: version "4.6.1" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" @@ -6213,7 +6250,7 @@ lodash.templatesettings@~2.3.0: lodash._reinterpolate "~2.3.0" lodash.escape "~2.3.0" -lodash.uniq@^4.2.0: +lodash.uniq@^4.2.0, lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= @@ -6631,6 +6668,11 @@ node-environment-flags@1.0.4: dependencies: object.getownpropertydescriptors "^2.0.3" +node-fetch@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" + integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"