Merge pull request #981 from theseyi/api-exception-handler

fixes deprecation update. adds api error handler. refactors misc sour…
This commit is contained in:
Seyi Adebajo 2018-02-21 15:31:35 -08:00 committed by GitHub
commit e3672d8837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 135 additions and 153 deletions

View File

@ -1,21 +1,14 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { inject } from '@ember/service';
import { getProperties, computed, set } from '@ember/object'; import { getProperties, computed, set } from '@ember/object';
import ComputedProperty, { oneWay } from '@ember/object/computed'; import ComputedProperty, { oneWay } from '@ember/object/computed';
import { baseCommentEditorOptions } from 'wherehows-web/constants'; import { baseCommentEditorOptions } from 'wherehows-web/constants';
import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications'; import { action } from 'ember-decorators/object';
export default class DatasetDeprecation extends Component { export default class DatasetDeprecation extends Component {
tagName = 'div'; tagName = 'div';
classNames = ['dataset-deprecation-toggle']; classNames = ['dataset-deprecation-toggle'];
/**
* References the application notifications service
* @memberof DatasetDeprecation
*/
notifications = <ComputedProperty<Notifications>>inject();
/** /**
* Flag indicating that the dataset is deprecated or otherwise * Flag indicating that the dataset is deprecated or otherwise
* @type {(null | boolean)} * @type {(null | boolean)}
@ -68,11 +61,11 @@ export default class DatasetDeprecation extends Component {
); );
/** /**
* The action to be completed when a save is initiated * The external action to be completed when a save is initiated
* @type {Function} * @type {(isDeprecated: boolean, updateDeprecationNode: string) => Promise<void>}
* @memberof DatasetDeprecation * @memberof DatasetDeprecation
*/ */
onUpdateDeprecation: Function; onUpdateDeprecation: (isDeprecated: boolean, updateDeprecationNode: string) => Promise<void> | void;
editorOptions = { editorOptions = {
...baseCommentEditorOptions, ...baseCommentEditorOptions,
@ -81,42 +74,29 @@ export default class DatasetDeprecation extends Component {
} }
}; };
actions = { /**
/** * Toggles the boolean value of deprecatedAlias
* Toggles the boolean value of deprecatedAlias */
*/ @action
toggleDeprecatedStatus(this: DatasetDeprecation) { toggleDeprecatedStatus(this: DatasetDeprecation) {
this.toggleProperty('deprecatedAlias'); this.toggleProperty('deprecatedAlias');
}, }
/** /**
* Invokes the save action with the updated values for * Invokes the save action with the updated values for
* deprecated and deprecationNote * deprecated and deprecationNote
*/ * @return {Promise<void>}
async onSave(this: DatasetDeprecation) { */
const { deprecatedAlias, deprecationNoteAlias, notifications: { notify } } = getProperties(this, [ @action
'deprecatedAlias', async onSave(this: DatasetDeprecation) {
'deprecationNoteAlias', const { deprecatedAlias, deprecationNoteAlias } = getProperties(this, ['deprecatedAlias', 'deprecationNoteAlias']);
'notifications' const { onUpdateDeprecation } = this;
]);
const { onUpdateDeprecation } = this;
if (onUpdateDeprecation) { if (onUpdateDeprecation) {
const noteValue = deprecatedAlias ? deprecationNoteAlias : ''; const noteValue = deprecatedAlias ? deprecationNoteAlias : '';
try { await onUpdateDeprecation(deprecatedAlias, noteValue);
await onUpdateDeprecation(deprecatedAlias, noteValue); set(this, 'deprecationNoteAlias', noteValue);
set(this, 'deprecationNoteAlias', noteValue);
notify(NotificationEvent.success, {
content: 'Successfully updated deprecation status'
});
} catch (e) {
notify(NotificationEvent.error, {
content: `An error occurred: ${e.message}`
});
}
}
} }
}; }
} }

View File

@ -146,20 +146,20 @@ export default class DatasetComplianceContainer extends Component {
setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless }); setProperties(this, { schemaFieldNamesMappedToDataTypes, schemaless });
}); });
@action
/** /**
* Persists the updates to the compliance policy on the remote host * Persists the updates to the compliance policy on the remote host
* @param {IComplianceInfo} complianceInfo * @param {IComplianceInfo} complianceInfo
* @return {Promise<void>} * @return {Promise<void>}
*/ */
@action
savePrivacyCompliancePolicy(complianceInfo: IComplianceInfo): Promise<void> { savePrivacyCompliancePolicy(complianceInfo: IComplianceInfo): Promise<void> {
return saveDatasetComplianceByUrn(get(this, 'urn'), complianceInfo); return saveDatasetComplianceByUrn(get(this, 'urn'), complianceInfo);
} }
@action
/** /**
* Resets the compliance information for the dataset with the previously persisted properties * Resets the compliance information for the dataset with the previously persisted properties
*/ */
@action
resetPrivacyCompliancePolicy() { resetPrivacyCompliancePolicy() {
get(this, 'getComplianceTask').perform(); get(this, 'getComplianceTask').perform();
} }

View File

@ -68,12 +68,12 @@ export default class DatasetOwnershipContainer extends Component {
set(this, 'ownerTypes', ownerTypes); set(this, 'ownerTypes', ownerTypes);
}); });
@action
/** /**
* Persists the changes to the owners list * Persists the changes to the owners list
* @param {Array<IOwner>} updatedOwners * @param {Array<IOwner>} updatedOwners
* @return {Promise<void>} * @return {Promise<void>}
*/ */
@action
saveOwnerChanges(updatedOwners: Array<IOwner>): Promise<void> { saveOwnerChanges(updatedOwners: Array<IOwner>): Promise<void> {
return updateDatasetOwnersByUrn(get(this, 'urn'), '', updatedOwners); return updateDatasetOwnersByUrn(get(this, 'urn'), '', updatedOwners);
} }

View File

@ -1,7 +1,10 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { get, setProperties } from '@ember/object'; import { get, setProperties } from '@ember/object';
import ComputedProperty from '@ember/object/computed';
import { inject } from '@ember/service';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { action } from 'ember-decorators/object'; import { action } from 'ember-decorators/object';
import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications';
import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset'; import { IDatasetView } from 'wherehows-web/typings/api/datasets/dataset';
import { readDatasetByUrn } from 'wherehows-web/utils/api/datasets/dataset'; import { readDatasetByUrn } from 'wherehows-web/utils/api/datasets/dataset';
import { updateDatasetDeprecationByUrn } from 'wherehows-web/utils/api/datasets/properties'; import { updateDatasetDeprecationByUrn } from 'wherehows-web/utils/api/datasets/properties';
@ -31,6 +34,13 @@ export default class DatasetPropertiesContainer extends Component {
*/ */
properties: Array<never> = []; properties: Array<never> = [];
/**
* References the application notifications service
* @memberof DatasetPropertiesContainer
* @type {ComputedProperty<Notifications>}
*/
notifications = <ComputedProperty<Notifications>>inject();
constructor() { constructor() {
super(...arguments); super(...arguments);
this.deprecationNote || (this.deprecationNote = ''); this.deprecationNote || (this.deprecationNote = '');
@ -55,15 +65,33 @@ export default class DatasetPropertiesContainer extends Component {
setProperties(this, { deprecated, deprecationNote }); setProperties(this, { deprecated, deprecationNote });
}); });
@action
/** /**
* Persists the changes to the dataset deprecation properties upstream * Persists the changes to the dataset deprecation properties upstream
* @param {boolean} isDeprecated * @param {boolean} isDeprecated
* @param {string} updatedDeprecationNote * @param {string} updatedDeprecationNote
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async updateDeprecation(isDeprecated: boolean, updatedDeprecationNote: string): Promise<void> { @action
await updateDatasetDeprecationByUrn(get(this, 'urn'), isDeprecated, updatedDeprecationNote); async updateDeprecation(
get(this, 'getDeprecationPropertiesTask').perform(); this: DatasetPropertiesContainer,
isDeprecated: boolean,
updatedDeprecationNote: string
): Promise<void> {
const { notify } = get(this, 'notifications');
try {
await updateDatasetDeprecationByUrn(get(this, 'urn'), isDeprecated, updatedDeprecationNote);
notify(NotificationEvent.success, {
content: 'Successfully updated deprecation status'
});
} catch (e) {
notify(NotificationEvent.error, {
content: `An error occurred: ${e.message}`
});
} finally {
// set current state
get(this, 'getDeprecationPropertiesTask').perform();
}
} }
} }

View File

@ -1,23 +0,0 @@
import Component from '@ember/component';
import { connect } from 'ember-redux';
/**
* A Selector function that takes the Redux Store and applies
* store state to props
* @param {Object} datasets is the slice of the store containing datasets
* and related state
* @return {{datasets: (any[]|Array), isFetching: boolean}}
*/
const stateToComputed = ({ datasets }) => {
const { byPage, byId, currentPage, isFetching = false } = datasets;
// List of datasets for the current Page
const pagedDatasetIds = byPage[currentPage] || [];
return {
// Takes the normalized list of ids and maps to dataset objects
datasets: pagedDatasetIds.map(datasetId => byId[datasetId]),
isFetching
};
};
export default connect(stateToComputed)(Component.extend({}));

View File

@ -0,0 +1,14 @@
import { ApiStatusNumber } from 'wherehows-web/utils/api/shared';
/**
* Returns a default msg for a given status
* @param {ApiStatusNumber} status
* @returns {string}
*/
const apiErrorStatusMessage = (status: ApiStatusNumber): string =>
(<{ [prop: number]: string }>{
[ApiStatusNumber.NotFound]: 'Could not find the requested resource',
[ApiStatusNumber.InternalServerError]: 'An error occurred with the server'
})[status];
export { apiErrorStatusMessage };

View File

@ -25,8 +25,6 @@ interface IPropertyItem {
*/ */
const datasetPropertiesUrlById = (id: number) => `${datasetUrlById(id)}/properties`; const datasetPropertiesUrlById = (id: number) => `${datasetUrlById(id)}/properties`;
const datasetDeprecationUrlById = (id: number) => `${datasetUrlById(id)}/deprecate`;
/** /**
* Returns the url for a dataset deprecation endpoint by urn * Returns the url for a dataset deprecation endpoint by urn
* @param {string} urn * @param {string} urn
@ -143,7 +141,7 @@ const readNonPinotProperties = async (id: number): Promise<Array<IPropertyItem>>
}; };
/** /**
* Describes the inteface of object returned from the api request to get pinot properties * Describes the interface of object returned from the api request to get pinot properties
* @interface IDatasetSamplesAndColumns * @interface IDatasetSamplesAndColumns
*/ */
interface IDatasetSamplesAndColumns { interface IDatasetSamplesAndColumns {
@ -184,26 +182,6 @@ const readPinotProperties = async (id: number) => {
} }
}; };
/**
* Updates the properties on the dataset for deprecation
* @param {number} id the id of the dataset
* @param {boolean} deprecated flag indicating deprecation
* @param {string} [deprecationNote=''] optional note accompanying deprecation change
*/
const updateDatasetDeprecation = async (id: number, deprecated: boolean, deprecationNote: string = '') => {
const { status, msg } = await putJSON<{ status: ApiStatus; msg: string }>({
url: datasetDeprecationUrlById(id),
data: {
deprecated,
deprecationNote
}
});
if (status !== ApiStatus.OK) {
throw new Error(msg);
}
};
/** /**
* Persists the changes to a datasets deprecation properties by urn * Persists the changes to a datasets deprecation properties by urn
* @param {string} urn * @param {string} urn
@ -211,24 +189,13 @@ const updateDatasetDeprecation = async (id: number, deprecated: boolean, depreca
* @param {string} deprecationNote * @param {string} deprecationNote
* @return {Promise<void>} * @return {Promise<void>}
*/ */
const updateDatasetDeprecationByUrn = ( const updateDatasetDeprecationByUrn = (urn: string, deprecated: boolean, deprecationNote: string = ''): Promise<void> =>
urn: string, putJSON<void>({
deprecated: boolean,
deprecationNote: string = ''
): Promise<void> => {
return putJSON<void>({
url: datasetDeprecationUrlByUrn(urn), url: datasetDeprecationUrlByUrn(urn),
data: { data: {
deprecated, deprecated,
deprecationNote deprecationNote
} }
}); });
};
export { export { readDatasetProperties, readNonPinotProperties, readPinotProperties, updateDatasetDeprecationByUrn };
readDatasetProperties,
readNonPinotProperties,
readPinotProperties,
updateDatasetDeprecation,
updateDatasetDeprecationByUrn
};

View File

@ -0,0 +1,21 @@
import { apiErrorStatusMessage } from 'wherehows-web/constants/errors/errors';
/**
* Wraps a Response object, pass through json response if no api error,
* otherwise raise exception with error message
* @template T
* @param {Response} response
* @returns {Promise<T>}
*/
const throwIfApiError = async <T>(response: Response): Promise<T> => {
const { status, ok } = response;
if (!ok) {
const { msg = apiErrorStatusMessage(status) } = await response.json();
throw new Error(msg);
}
return response.json();
};
export { throwIfApiError };

View File

@ -1,4 +1,5 @@
import fetch from 'fetch'; import fetch from 'fetch';
import { throwIfApiError } from 'wherehows-web/utils/api/errors/errors';
/** /**
* Describes the attributes on the fetch configuration object * Describes the attributes on the fetch configuration object
@ -10,7 +11,7 @@ interface FetchConfig {
} }
/** /**
* Desribes the available options on an option bag to be passed into a fetch call * Describes the available options on an option bag to be passed into a fetch call
* @interface IFetchOptions * @interface IFetchOptions
*/ */
interface IFetchOptions { interface IFetchOptions {
@ -38,11 +39,11 @@ const withBaseFetchHeaders = (headers: FetchConfig['headers']): { headers: Fetch
* Sends a HTTP request and resolves with the JSON response * Sends a HTTP request and resolves with the JSON response
* @template T * @template T
* @param {string} url the url for the endpoint to request a response from * @param {string} url the url for the endpoint to request a response from
* @param {object} fetchConfig * @param {object} fetchConfig
* @returns {Promise<T>} * @returns {Promise<T>}
*/ */
const json = <T>(url: string = '', fetchConfig: IFetchOptions = {}): Promise<T> => const json = <T>(url: string = '', fetchConfig: IFetchOptions = {}): Promise<T> =>
fetch(url, fetchConfig).then<T>(response => response.json()); fetch(url, fetchConfig).then<T>(response => throwIfApiError(response));
/** /**
* Conveniently gets a JSON response using the fetch api * Conveniently gets a JSON response using the fetch api
@ -59,8 +60,8 @@ const getJSON = <T>(config: FetchConfig): Promise<T> => {
/** /**
* Initiates a POST request using the Fetch api * Initiates a POST request using the Fetch api
* @template T * @template T
* @param {FetchConfig} config * @param {FetchConfig} config
* @returns {Promise<T>} * @returns {Promise<T>}
*/ */
const postJSON = <T>(config: FetchConfig): Promise<T> => { const postJSON = <T>(config: FetchConfig): Promise<T> => {
const requestBody = config.data ? { body: JSON.stringify(config.data) } : {}; const requestBody = config.data ? { body: JSON.stringify(config.data) } : {};
@ -120,22 +121,4 @@ const getHeaders = async (config: FetchConfig): Promise<Headers> => {
throw new Error(statusText); throw new Error(statusText);
}; };
/** export { getJSON, postJSON, deleteJSON, putJSON, getHeaders };
* Wraps a request Promise, pass-through response if successful, otherwise handle the error and rethrow if not api error
* @template T
* @param {Promise<T>} fetcher the api request to wrap
* @param {T} defaultValue
* @returns {Promise<T|null>}
*/
const fetchAndHandleIfApiError = async <T>(fetcher: Promise<T>, defaultValue: T): Promise<T | null> => {
let result = typeof defaultValue === 'undefined' ? null : defaultValue;
try {
result = await fetcher;
} catch (e) {
// TODO: if error is an api error, display notification and allow default return
// otherwise throw
}
return result;
};
export { getJSON, postJSON, deleteJSON, putJSON, getHeaders, fetchAndHandleIfApiError };

View File

@ -21,3 +21,13 @@ export enum ApiStatus {
FAILED = 'failed', FAILED = 'failed',
ERROR = 'error' ERROR = 'error'
} }
/**
* Enumerates the currently available Api statuses
* @type {number}
*/
export enum ApiStatusNumber {
NotFound = 404,
UnAuthorized = 401,
InternalServerError = 500
}

View File

@ -5,13 +5,7 @@ import hbs from 'htmlbars-inline-precompile';
import { run } from '@ember/runloop'; import { run } from '@ember/runloop';
moduleForComponent('dataset-deprecation', 'Integration | Component | dataset deprecation', { moduleForComponent('dataset-deprecation', 'Integration | Component | dataset deprecation', {
integration: true, integration: true
beforeEach() {
this.register('service:notifications', notificationsStub);
this.inject.service('notifications');
}
}); });
test('it renders', function(assert) { test('it renders', function(assert) {

View File

@ -0,0 +1,15 @@
import { throwIfApiError } from 'wherehows-web/utils/api/errors/errors';
import { module, test } from 'qunit';
module('Unit | Utility | api/errors/errors');
test('throwIfApiError exists', function(assert) {
assert.ok(typeof throwIfApiError === 'function', 'throwIfApiError exists as a function');
});
test('throwIfApiError returns a Promise / thennable', function(assert) {
assert.ok(
typeof throwIfApiError({ status: 200, ok: true, json: () => Promise.resolve() }).then === 'function',
'invocation returns a Promise object / thennable'
);
});

View File

@ -1,11 +1,4 @@
import { import { getJSON, postJSON, deleteJSON, putJSON, getHeaders } from 'wherehows-web/utils/api/fetcher';
getJSON,
postJSON,
deleteJSON,
putJSON,
getHeaders,
fetchAndHandleIfApiError
} from 'wherehows-web/utils/api/fetcher';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import sinon from 'sinon'; import sinon from 'sinon';
@ -20,7 +13,7 @@ module('Unit | Utility | api/fetcher', {
}); });
test('each http request function exists', function(assert) { test('each http request function exists', function(assert) {
[getJSON, postJSON, deleteJSON, putJSON, getHeaders, fetchAndHandleIfApiError].forEach(httpRequest => [getJSON, postJSON, deleteJSON, putJSON, getHeaders].forEach(httpRequest =>
assert.ok(typeof httpRequest === 'function', `${httpRequest} is a function`) assert.ok(typeof httpRequest === 'function', `${httpRequest} is a function`)
); );
}); });