From dbd6487b0b430cc57db573193ff224d550c84bbd Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 2 Jan 2020 10:59:13 +0000 Subject: [PATCH 1/2] Add API helpers --- app/src/frontend/apiHelpers.ts | 44 ++++++++++ app/src/frontend/building/data-container.tsx | 27 ++---- .../building/edit-history/edit-history.tsx | 6 +- app/src/frontend/map-app.tsx | 84 ++++--------------- app/src/frontend/map/map.tsx | 17 ++-- app/src/frontend/map/search-box.tsx | 8 +- app/src/frontend/pages/changes.tsx | 6 +- app/src/frontend/pages/data-extracts.tsx | 7 +- app/src/frontend/user/login.tsx | 22 ++--- app/src/frontend/user/my-account.tsx | 25 ++---- app/src/frontend/user/signup.tsx | 36 +++----- 11 files changed, 111 insertions(+), 171 deletions(-) create mode 100644 app/src/frontend/apiHelpers.ts diff --git a/app/src/frontend/apiHelpers.ts b/app/src/frontend/apiHelpers.ts new file mode 100644 index 00000000..5be97cad --- /dev/null +++ b/app/src/frontend/apiHelpers.ts @@ -0,0 +1,44 @@ +type JsonReviver = (name: string, value: any) => any; + +export function apiGet(path: string, options?: { + jsonReviver?: JsonReviver +}): Promise { + return apiRequest(path, 'GET', null, options); +} + +export function apiPost(path: string, data?: object, options?: { + jsonReviver?: JsonReviver +}): Promise { + return apiRequest(path, 'POST', data, options); +} + +export function apiDelete(path: string, options?: { + jsonReviver?: JsonReviver +}): Promise { + return apiRequest(path, 'DELETE', null, options); +} + +async function apiRequest( + path: string, + method: 'GET' | 'POST' | 'DELETE', + data?: object, + options?: { + jsonReviver?: JsonReviver + } +): Promise { + const res = await fetch(path, { + method: method, + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + body: data == undefined ? null : JSON.stringify(data), + }); + + const reviver = options?.jsonReviver; + if (reviver != undefined) { + return JSON.parse(await res.text(), reviver); + } else { + return await res.json(); + } +} diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 13b2cc27..f84d2ccd 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -1,6 +1,7 @@ import React, { Fragment } from 'react'; import { NavLink, Redirect } from 'react-router-dom'; +import { apiPost } from '../apiHelpers'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; import { compareObjects } from '../helpers'; @@ -162,15 +163,10 @@ const withCopyEdit = (WrappedComponent: React.ComponentType) */ async handleLike(like: boolean) { try { - const res = await fetch(`/api/buildings/${this.props.building.building_id}/like.json`, { - method: 'POST', - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin', - body: JSON.stringify({like: like}) - }); - const data = await res.json(); + const data = await apiPost( + `/api/buildings/${this.props.building.building_id}/like.json`, + {like: like} + ); if (data.error) { this.setState({error: data.error}); @@ -188,15 +184,10 @@ const withCopyEdit = (WrappedComponent: React.ComponentType) this.setState({error: undefined}); try { - const res = await fetch(`/api/buildings/${this.props.building.building_id}.json`, { - method: 'POST', - body: JSON.stringify(this.state.buildingEdits), - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }); - const data = await res.json(); + const data = await apiPost( + `/api/buildings/${this.props.building.building_id}.json`, + this.state.buildingEdits + ); if (data.error) { this.setState({error: data.error}); diff --git a/app/src/frontend/building/edit-history/edit-history.tsx b/app/src/frontend/building/edit-history/edit-history.tsx index c6a160b6..628bd2a1 100644 --- a/app/src/frontend/building/edit-history/edit-history.tsx +++ b/app/src/frontend/building/edit-history/edit-history.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import './edit-history.css'; +import { apiGet } from '../../apiHelpers'; import { Building } from '../../models/building'; import { EditHistoryEntry } from '../../models/edit-history-entry'; import ContainerHeader from '../container-header'; @@ -17,10 +18,9 @@ const EditHistory: React.FunctionComponent = (props) => { useEffect(() => { const fetchData = async () => { - const res = await fetch(`/api/buildings/${props.building.building_id}/history.json`); - const data = await res.json(); + const {history} = await apiGet(`/api/buildings/${props.building.building_id}/history.json`); - setHistory(data.history); + setHistory(history); }; if (props.building != undefined) { // only call fn if there is a building provided diff --git a/app/src/frontend/map-app.tsx b/app/src/frontend/map-app.tsx index f79bad1b..d1923093 100644 --- a/app/src/frontend/map-app.tsx +++ b/app/src/frontend/map-app.tsx @@ -5,6 +5,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { parseJsonOrDefault } from '../helpers'; import { strictParseInt } from '../parse'; +import { apiGet, apiPost } from './apiHelpers'; import BuildingView from './building/building-view'; import Categories from './building/categories'; import { EditHistory } from './building/edit-history/edit-history'; @@ -64,16 +65,9 @@ class MapApp extends React.Component { async fetchLatestRevision() { try { - const res = await fetch(`/api/buildings/revision`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }); - const data = await res.json(); + const {latestRevisionId} = await apiGet(`/api/buildings/revision`); - this.increaseRevision(data.latestRevisionId); + this.increaseRevision(latestRevisionId); } catch(error) { console.error(error); } @@ -89,27 +83,9 @@ class MapApp extends React.Component { // TODO: simplify API calls, create helpers for fetching data const buildingId = strictParseInt(this.props.match.params.building); let [building, building_uprns, building_like] = await Promise.all([ - fetch(`/api/buildings/${buildingId}.json`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then(res => res.json()), - fetch(`/api/buildings/${buildingId}/uprns.json`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then(res => res.json()), - fetch(`/api/buildings/${buildingId}/like.json`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then(res => res.json()) + apiGet(`/api/buildings/${buildingId}.json`), + apiGet(`/api/buildings/${buildingId}/uprns.json`), + apiGet(`/api/buildings/${buildingId}/like.json`) ]); building.uprns = building_uprns.uprns; @@ -158,15 +134,8 @@ class MapApp extends React.Component { this.increaseRevision(building.revision_id); // get UPRNs and update - fetch(`/api/buildings/${building.building_id}/uprns.json`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then((res) => { + apiGet(`/api/buildings/${building.building_id}/uprns.json`) + .then((res) => { if (res.error) { console.error(res); } else { @@ -179,15 +148,8 @@ class MapApp extends React.Component { }); // get if liked and update - fetch(`/api/buildings/${building.building_id}/like.json`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then((res) => { + apiGet(`/api/buildings/${building.building_id}/like.json`) + .then((res) => { if (res.error) { console.error(res); } else { @@ -225,37 +187,21 @@ class MapApp extends React.Component { } likeBuilding(buildingId) { - fetch(`/api/buildings/${buildingId}/like.json`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin', - body: JSON.stringify({ like: true }) - }).then( - res => res.json() - ).then(function (res) { + apiPost(`/api/buildings/${buildingId}/like.json`, { like: true }) + .then(res => { if (res.error) { console.error({ error: res.error }); } else { this.increaseRevision(res.revision_id); } - }.bind(this)).catch( + }).catch( (err) => console.error({ error: err }) ); } updateBuilding(buildingId, data) { - fetch(`/api/buildings/${buildingId}.json`, { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then(res => { + apiPost(`/api/buildings/${buildingId}.json`, data) + .then(res => { if (res.error) { console.error({ error: res.error }); } else { diff --git a/app/src/frontend/map/map.tsx b/app/src/frontend/map/map.tsx index 353b42b7..46eee97a 100644 --- a/app/src/frontend/map/map.tsx +++ b/app/src/frontend/map/map.tsx @@ -5,6 +5,7 @@ import { AttributionControl, GeoJSON, Map, TileLayer, ZoomControl } from 'react- import 'leaflet/dist/leaflet.css'; import './map.css'; +import { apiGet } from '../apiHelpers'; import { HelpIcon } from '../components/icons'; import { Building } from '../models/building'; @@ -58,13 +59,9 @@ class ColouringMap extends Component { handleClick(e) { const mode = this.props.mode; - const lat = e.latlng.lat; - const lng = e.latlng.lng; - fetch( - '/api/buildings/locate?lat='+lat+'&lng='+lng - ).then( - (res) => res.json() - ).then(function(data){ + const { lat, lng } = e.latlng; + apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`) + .then(data => { if (data && data.length){ const building = data[0]; if (mode === 'multi-edit') { @@ -82,7 +79,7 @@ class ColouringMap extends Component { this.props.selectBuilding(undefined); } } - }.bind(this)).catch( + }).catch( (err) => console.error(err) ); } @@ -94,8 +91,8 @@ class ColouringMap extends Component { } async getBoundary() { - const res = await fetch('/geometries/boundary-detailed.geojson'); - const data = await res.json() as GeoJsonObject; + const data = await apiGet('/geometries/boundary-detailed.geojson') as GeoJsonObject; + this.setState({ boundary: data }); diff --git a/app/src/frontend/map/search-box.tsx b/app/src/frontend/map/search-box.tsx index 02945359..376628c9 100644 --- a/app/src/frontend/map/search-box.tsx +++ b/app/src/frontend/map/search-box.tsx @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import './search-box.css'; +import { apiGet } from '../apiHelpers'; import { SearchIcon } from '../components/icons'; interface SearchResult { @@ -96,11 +97,8 @@ class SearchBox extends Component { fetching: true }); - fetch( - '/api/search?q='+this.state.q - ).then( - (res) => res.json() - ).then((data) => { + apiGet(`/api/search?q=${this.state.q}`) + .then((data) => { if (data && data.results){ this.setState({ results: data.results, diff --git a/app/src/frontend/pages/changes.tsx b/app/src/frontend/pages/changes.tsx index 2e4f6d8d..6f0d7cc5 100644 --- a/app/src/frontend/pages/changes.tsx +++ b/app/src/frontend/pages/changes.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; +import { apiGet } from '../apiHelpers'; import { BuildingEditSummary } from '../building/edit-history/building-edit-summary'; import InfoBox from '../components/info-box'; import { EditHistoryEntry } from '../models/edit-history-entry'; @@ -9,10 +10,9 @@ const ChangesPage = () => { useEffect(() => { const fetchData = async () => { - const res = await fetch(`/api/history`); - const data = await res.json(); + const {history} = await apiGet(`/api/history`); - setHistory(data.history); + setHistory(history); }; fetchData(); diff --git a/app/src/frontend/pages/data-extracts.tsx b/app/src/frontend/pages/data-extracts.tsx index 3482475f..a5f84eb8 100644 --- a/app/src/frontend/pages/data-extracts.tsx +++ b/app/src/frontend/pages/data-extracts.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent } from 'react'; import { Link } from 'react-router-dom'; import { dateReviver } from '../../helpers'; +import { apiGet } from '../apiHelpers'; interface ExtractViewModel { @@ -28,11 +29,9 @@ export default class DataExtracts extends React.Component<{}, DataExtractsState> } async componentDidMount() { - const res = await fetch('/api/extracts'); - const data = JSON.parse(await res.text(), dateReviver); + let data = await apiGet('/api/extracts', { jsonReviver: dateReviver}); const extracts = (data.extracts as ExtractViewModel[]) - .sort((a, b) => a.extracted_on.valueOf() - b.extracted_on.valueOf()) - .reverse(); + .sort((a, b) => b.extracted_on.valueOf() - a.extracted_on.valueOf()); diff --git a/app/src/frontend/user/login.tsx b/app/src/frontend/user/login.tsx index ca36b5ce..99ebcc29 100644 --- a/app/src/frontend/user/login.tsx +++ b/app/src/frontend/user/login.tsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { Link, Redirect } from 'react-router-dom'; +import { apiGet, apiPost } from '../apiHelpers'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; import SupporterLogos from '../components/supporter-logos'; @@ -39,24 +40,13 @@ class Login extends Component { event.preventDefault(); this.setState({error: undefined}); - fetch('/api/login', { - method: 'POST', - body: JSON.stringify(this.state), - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then(function(res){ + apiPost('/api/login', this.state) + .then(res => { if (res.error) { this.setState({error: res.error}); } else { - fetch('/api/users/me', { - credentials: 'same-origin' - }).then( - (res) => res.json() - ).then(user => { + apiGet('/api/users/me') + .then(user => { if (user.error) { this.setState({error: user.error}); } else { @@ -66,7 +56,7 @@ class Login extends Component { (err) => this.setState({error: err}) ); } - }.bind(this)).catch( + }).catch( (err) => this.setState({error: err}) ); } diff --git a/app/src/frontend/user/my-account.tsx b/app/src/frontend/user/my-account.tsx index b4676fbc..9dbd0f7b 100644 --- a/app/src/frontend/user/my-account.tsx +++ b/app/src/frontend/user/my-account.tsx @@ -1,6 +1,7 @@ import React, { Component, FormEvent } from 'react'; import { Link, Redirect } from 'react-router-dom'; +import { apiDelete, apiPost } from '../apiHelpers'; import ConfirmationModal from '../components/confirmation-modal'; import ErrorBox from '../components/error-box'; import { User } from '../models/user'; @@ -32,12 +33,8 @@ class MyAccountPage extends Component { event.preventDefault(); this.setState({error: undefined}); - fetch('/api/logout', { - method: 'POST', - credentials: 'same-origin' - }).then( - res => res.json() - ).then(function(res){ + apiPost('/api/logout') + .then(function(res){ if (res.error) { this.setState({error: res.error}); } else { @@ -52,18 +49,14 @@ class MyAccountPage extends Component { event.preventDefault(); this.setState({error: undefined}); - fetch('/api/api/key', { - method: 'POST', - credentials: 'same-origin' - }).then( - res => res.json() - ).then(function(res){ + apiPost('/api/api/key') + .then(res => { if (res.error) { this.setState({error: res.error}); } else { this.props.updateUser(res); } - }.bind(this)).catch( + }).catch( (err) => this.setState({error: err}) ); } @@ -81,11 +74,7 @@ class MyAccountPage extends Component { this.setState({ error: undefined }); try { - const res = await fetch('/api/users/me', { - method: 'DELETE', - credentials: 'same-origin' - }); - const data = await res.json(); + const data = await apiDelete('/api/users/me'); if(data.error) { this.setState({ error: data.error }); diff --git a/app/src/frontend/user/signup.tsx b/app/src/frontend/user/signup.tsx index cf422163..a6060568 100644 --- a/app/src/frontend/user/signup.tsx +++ b/app/src/frontend/user/signup.tsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { Link, Redirect } from 'react-router-dom'; +import { apiGet, apiPost } from '../apiHelpers'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; import SupporterLogos from '../components/supporter-logos'; @@ -48,36 +49,21 @@ class SignUp extends Component { } as Pick); } - handleSubmit(event) { + async handleSubmit(event) { event.preventDefault(); this.setState({error: undefined}); - fetch('/api/users', { - method: 'POST', - body: JSON.stringify(this.state), - headers:{ - 'Content-Type': 'application/json' - }, - credentials: 'same-origin' - }).then( - res => res.json() - ).then(function(res){ - if (res.error) { - this.setState({error: res.error}); + try { + const res = await apiPost('/api/users', this.state); + if(res.error) { + this.setState({ error: res.error }); } else { - fetch('/api/users/me', { - credentials: 'same-origin' - }).then( - (res) => res.json() - ).then( - (user) => this.props.login(user) - ).catch( - (err) => this.setState({error: err}) - ); + const user = await apiGet('/api/users/me'); + this.props.login(user); } - }.bind(this)).catch( - (err) => this.setState({error: err}) - ); + } catch(err) { + this.setState({error: err}); + } } render() { From beea6675cb3557303b447c44bfa053656b7120ce Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 2 Jan 2020 12:23:12 +0000 Subject: [PATCH 2/2] Remove null-conditional operator --- app/src/frontend/apiHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/frontend/apiHelpers.ts b/app/src/frontend/apiHelpers.ts index 5be97cad..b35ecd7f 100644 --- a/app/src/frontend/apiHelpers.ts +++ b/app/src/frontend/apiHelpers.ts @@ -35,7 +35,7 @@ async function apiRequest( body: data == undefined ? null : JSON.stringify(data), }); - const reviver = options?.jsonReviver; + const reviver = options == undefined ? undefined : options.jsonReviver; if (reviver != undefined) { return JSON.parse(await res.text(), reviver); } else {