Skip to content

Commit

Permalink
Add types and tslint
Browse files Browse the repository at this point in the history
  • Loading branch information
xg-wang committed Jan 29, 2019
1 parent 7b95d31 commit 1b328e3
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 100 deletions.
10 changes: 10 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ module.exports = {
'no-console': ["error", { allow: ['warn'] }]
},
overrides: [
// TypeScript files
{
files: ['addon/**/*.ts'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
'no-undef': 'off',
'no-unused-var': 'off'
}
},
// node files
{
files: [
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Available imports:
import fetch, { Headers, Request, Response, AbortController } from 'fetch';
```

### Use with TypeScript
To use `ember-fetch` with TypeScript or enable editor's type support, add `"fetch": "ember-cli/ember-fetch"` to your app's `devDependencies`.
This will get the current state of `ember-fetch` from this GitHub repo as a dependency.

You can also add `"fetch": ["node_modules/ember-fetch"]` to your `tsconfig.json`.

### 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.

Expand Down
143 changes: 81 additions & 62 deletions addon/mixins/adapter-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,52 @@
import Mixin from '@ember/object/mixin';
import { assign } from '@ember/polyfills'
import RSVP from 'rsvp';
import { assign } from '@ember/polyfills';
import RSVP, { reject } from 'rsvp';
import fetch from 'fetch';
import mungOptionsForFetch from '../utils/mung-options-for-fetch';
import determineBodyPromise from '../utils/determine-body-promise';
import DS from 'ember-data';
import Mix from '@ember/polyfills/types';
import { get } from '@ember/object';
import {
PlainObject,
PlainHeaders,
Method,
FetchOptions,
Nullable,
AjaxOptions
} from 'ember-fetch/types';

/**
* Helper function to create a plain object from the response's Headers.
* Consumed by the adapter's `handleResponse`.
* @param {Headers} headers
* @returns {Object}
*/
export function headersToObject(headers) {
let headersObject = {};
export function headersToObject(headers: Headers): PlainObject {
let headersObject: PlainObject = {};

if (headers) {
headers.forEach((value, key) => headersObject[key] = value);
headers.forEach((value, key) => (headersObject[key] = value));
}

return headersObject;
}

export default Mixin.create({
headers: undefined as undefined | PlainHeaders,
/**
* @param {String} url
* @param {String} type
* @param {Object} _options
* @returns {Object}
* @override
*/
ajaxOptions(url, type, options = {}) {
options.url = url;
options.type = type;
*/
ajaxOptions(url: string, type: Method, options: object): FetchOptions {
let hash = (options || {}) as AjaxOptions;
hash.url = url;
hash.type = type;

// Add headers set on the Adapter
let adapterHeaders = this.get('headers');
let adapterHeaders = get(this, 'headers');
if (adapterHeaders) {
options.headers = assign(options.headers || {}, adapterHeaders);
hash.headers = assign(hash.headers || {}, adapterHeaders);
}

const mungedOptions = mungOptionsForFetch(options);
const mungedOptions = mungOptionsForFetch(hash);

// Mimics the default behavior in Ember Data's `ajaxOptions`, namely to set the
// 'Content-Type' header to application/json if it is not a GET request and it has a body.
Expand All @@ -60,65 +67,73 @@ export default Mixin.create({
},

/**
* @param {String} url
* @param {String} type
* @param {Object} options
* @override
*/
ajax(url, type, options) {
ajax(url: string, type: Method, options: object) {
const requestData = {
url,
method: type,
method: type
};

const hash = this.ajaxOptions(url, type, options);

return this._ajaxRequest(hash)
.catch((error, response, requestData) => {
throw this.ajaxError(this, response, null, requestData, error);
})
.then((response) => {
return RSVP.hash({
response,
payload: determineBodyPromise(response, requestData)
});
})
.then(({ response, payload }) => {
if (response.ok) {
return this.ajaxSuccess(this, response, payload, requestData);
} else {
throw this.ajaxError(this, response, payload, requestData);
}
});
return (
this._ajaxRequest(hash)
// @ts-ignore
.catch((error, response, requestData) => {
throw this.ajaxError(this, response, null, requestData, error);
})
.then((response: Response) => {
return RSVP.hash({
response,
payload: determineBodyPromise(response, requestData)
});
})
.then(
({
response,
payload
}: {
response: Response;
payload: string | object | undefined;
}) => {
if (response.ok) {
return this.ajaxSuccess(this, response, payload, requestData);
} else {
throw this.ajaxError(this, response, payload, requestData);
}
}
)
);
},

/**
* Overrides the `_ajaxRequest` method to use `fetch` instead of jQuery.ajax
* @param {Object} options
* @override
*/
_ajaxRequest(options) {
_ajaxRequest(
options: Mix<RequestInit, { url: string }>
): RSVP.Promise<Response> {
return this._fetchRequest(options.url, options);
},

/**
* A hook into where `fetch` is called.
* Useful if you want to override this behavior, for example to multiplex requests.
* @param {String} url
* @param {Object} options
*/
_fetchRequest(url, options) {
_fetchRequest(url: string, options: RequestInit): RSVP.Promise<Response> {
return fetch(url, options);
},

/**
* @param {Object} adapter
* @param {Object} response
* @param {Object} payload
* @param {Object} requestData
* @override
*/
ajaxSuccess(adapter, response, payload, requestData) {
ajaxSuccess(
adapter: any,
response: Response,
payload: Nullable<string | object>,
requestData: { url: string; method: string }
): object | DS.AdapterError | RSVP.Promise<never> {
const returnResponse = adapter.handleResponse(
response.status,
headersToObject(response.headers),
Expand All @@ -127,36 +142,40 @@ export default Mixin.create({
);

if (returnResponse && returnResponse.isAdapterError) {
return RSVP.Promise.reject(returnResponse);
return reject(returnResponse);
} else {
return returnResponse;
}
},


/**
* Allows for the error to be selected from either the
* response object, or the response data.
* @param {Object} response
* @param {Object} payload
*/
parseFetchResponseForError(response, payload) {
parseFetchResponseForError(
response: Response,
payload: object | string
): object | string {
return payload || response.statusText;
},

/**
* @param {Object} adapter
* @param {Object} response
* @param {String|Object} payload
* @param {Object} requestData
* @param {Error} error
* @override
*/
ajaxError(adapter, response, payload, requestData, error) {
ajaxError(
adapter: any,
response: Response,
payload: Nullable<string | object>,
requestData: object,
error?: Error
): Error | object | DS.AdapterError {
if (error) {
return error;
} else {
const parsedResponse = adapter.parseFetchResponseForError(response, payload);
const parsedResponse = adapter.parseFetchResponseForError(
response,
payload
);
return adapter.handleResponse(
response.status,
headersToObject(response.headers),
Expand Down
36 changes: 36 additions & 0 deletions addon/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Mix from '@ember/polyfills/types';

export type Nullable<T> = T | null | undefined;

export interface PlainObject {
[key: string]: string | PlainObject | PlainObject[];
}

export interface PlainHeaders {
[key: string]: string | undefined | null;
}

export type Method =
| 'HEAD'
| 'GET'
| 'POST'
| 'PUT'
| 'PATCH'
| 'DELETE'
| 'OPTIONS';

export type AjaxOptions = {
url: string;
type: Method;
data?: PlainObject | BodyInit;
headers?: PlainHeaders;
};

export type FetchOptions = Mix<
AjaxOptions,
{ body?: BodyInit | null; method?: Method }
>;

export function isPlainObject(obj: any): obj is PlainObject {
return Object.prototype.toString.call(obj) === '[object Object]';
}
22 changes: 13 additions & 9 deletions addon/utils/determine-body-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,29 @@
* Function that always attempts to parse the response as json, and if an error is thrown,
* returns `undefined` if the response is successful and has a status code of 204 (No Content),
* or 205 (Reset Content) or if the request method was 'HEAD', and the plain payload otherwise.
* @param {Response} response
* @param {Object} requestData
* @returns {Promise}
*/
export default function determineBodyPromise(response, requestData) {
export default function determineBodyPromise(
response: Response,
requestData: JQueryAjaxSettings
): Promise<object | string | undefined> {
return response.text().then(function(payload) {
let ret: string | object | undefined = payload;
try {
payload = JSON.parse(payload);
} catch(error) {
ret = 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 = undefined;
if (
response.ok &&
(status === 204 || status === 205 || requestData.method === 'HEAD')
) {
ret = undefined;
} else {
console.warn('This response was unable to be parsed as json.', payload);
}
}
return payload;
return ret;
});
}

0 comments on commit 1b328e3

Please sign in to comment.