Merge pull request #537 from mz8i/feature/api-helpers

Feature: API helpers
This commit is contained in:
Maciej Ziarkowski 2020-01-02 16:55:47 +00:00 committed by GitHub
commit 35554e37ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 111 additions and 171 deletions

View File

@ -0,0 +1,44 @@
type JsonReviver = (name: string, value: any) => any;
export function apiGet(path: string, options?: {
jsonReviver?: JsonReviver
}): Promise<any> {
return apiRequest(path, 'GET', null, options);
}
export function apiPost(path: string, data?: object, options?: {
jsonReviver?: JsonReviver
}): Promise<any> {
return apiRequest(path, 'POST', data, options);
}
export function apiDelete(path: string, options?: {
jsonReviver?: JsonReviver
}): Promise<any> {
return apiRequest(path, 'DELETE', null, options);
}
async function apiRequest(
path: string,
method: 'GET' | 'POST' | 'DELETE',
data?: object,
options?: {
jsonReviver?: JsonReviver
}
): Promise<any> {
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 == undefined ? undefined : options.jsonReviver;
if (reviver != undefined) {
return JSON.parse(await res.text(), reviver);
} else {
return await res.json();
}
}

View File

@ -1,6 +1,7 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { NavLink, Redirect } from 'react-router-dom'; import { NavLink, Redirect } from 'react-router-dom';
import { apiPost } from '../apiHelpers';
import ErrorBox from '../components/error-box'; import ErrorBox from '../components/error-box';
import InfoBox from '../components/info-box'; import InfoBox from '../components/info-box';
import { compareObjects } from '../helpers'; import { compareObjects } from '../helpers';
@ -162,15 +163,10 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
*/ */
async handleLike(like: boolean) { async handleLike(like: boolean) {
try { try {
const res = await fetch(`/api/buildings/${this.props.building.building_id}/like.json`, { const data = await apiPost(
method: 'POST', `/api/buildings/${this.props.building.building_id}/like.json`,
headers:{ {like: like}
'Content-Type': 'application/json' );
},
credentials: 'same-origin',
body: JSON.stringify({like: like})
});
const data = await res.json();
if (data.error) { if (data.error) {
this.setState({error: data.error}); this.setState({error: data.error});
@ -188,15 +184,10 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
this.setState({error: undefined}); this.setState({error: undefined});
try { try {
const res = await fetch(`/api/buildings/${this.props.building.building_id}.json`, { const data = await apiPost(
method: 'POST', `/api/buildings/${this.props.building.building_id}.json`,
body: JSON.stringify(this.state.buildingEdits), this.state.buildingEdits
headers:{ );
'Content-Type': 'application/json'
},
credentials: 'same-origin'
});
const data = await res.json();
if (data.error) { if (data.error) {
this.setState({error: data.error}); this.setState({error: data.error});

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import './edit-history.css'; import './edit-history.css';
import { apiGet } from '../../apiHelpers';
import { Building } from '../../models/building'; import { Building } from '../../models/building';
import { EditHistoryEntry } from '../../models/edit-history-entry'; import { EditHistoryEntry } from '../../models/edit-history-entry';
import ContainerHeader from '../container-header'; import ContainerHeader from '../container-header';
@ -17,10 +18,9 @@ const EditHistory: React.FunctionComponent<EditHistoryProps> = (props) => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const res = await fetch(`/api/buildings/${props.building.building_id}/history.json`); const {history} = await apiGet(`/api/buildings/${props.building.building_id}/history.json`);
const data = await res.json();
setHistory(data.history); setHistory(history);
}; };
if (props.building != undefined) { // only call fn if there is a building provided if (props.building != undefined) { // only call fn if there is a building provided

View File

@ -5,6 +5,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { parseJsonOrDefault } from '../helpers'; import { parseJsonOrDefault } from '../helpers';
import { strictParseInt } from '../parse'; import { strictParseInt } from '../parse';
import { apiGet, apiPost } from './apiHelpers';
import BuildingView from './building/building-view'; import BuildingView from './building/building-view';
import Categories from './building/categories'; import Categories from './building/categories';
import { EditHistory } from './building/edit-history/edit-history'; import { EditHistory } from './building/edit-history/edit-history';
@ -64,16 +65,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
async fetchLatestRevision() { async fetchLatestRevision() {
try { try {
const res = await fetch(`/api/buildings/revision`, { const {latestRevisionId} = await apiGet(`/api/buildings/revision`);
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
});
const data = await res.json();
this.increaseRevision(data.latestRevisionId); this.increaseRevision(latestRevisionId);
} catch(error) { } catch(error) {
console.error(error); console.error(error);
} }
@ -89,27 +83,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
// TODO: simplify API calls, create helpers for fetching data // TODO: simplify API calls, create helpers for fetching data
const buildingId = strictParseInt(this.props.match.params.building); const buildingId = strictParseInt(this.props.match.params.building);
let [building, building_uprns, building_like] = await Promise.all([ let [building, building_uprns, building_like] = await Promise.all([
fetch(`/api/buildings/${buildingId}.json`, { apiGet(`/api/buildings/${buildingId}.json`),
method: 'GET', apiGet(`/api/buildings/${buildingId}/uprns.json`),
headers: { apiGet(`/api/buildings/${buildingId}/like.json`)
'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())
]); ]);
building.uprns = building_uprns.uprns; building.uprns = building_uprns.uprns;
@ -158,15 +134,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
this.increaseRevision(building.revision_id); this.increaseRevision(building.revision_id);
// get UPRNs and update // get UPRNs and update
fetch(`/api/buildings/${building.building_id}/uprns.json`, { apiGet(`/api/buildings/${building.building_id}/uprns.json`)
method: 'GET', .then((res) => {
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then((res) => {
if (res.error) { if (res.error) {
console.error(res); console.error(res);
} else { } else {
@ -179,15 +148,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
}); });
// get if liked and update // get if liked and update
fetch(`/api/buildings/${building.building_id}/like.json`, { apiGet(`/api/buildings/${building.building_id}/like.json`)
method: 'GET', .then((res) => {
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then((res) => {
if (res.error) { if (res.error) {
console.error(res); console.error(res);
} else { } else {
@ -225,37 +187,21 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
} }
likeBuilding(buildingId) { likeBuilding(buildingId) {
fetch(`/api/buildings/${buildingId}/like.json`, { apiPost(`/api/buildings/${buildingId}/like.json`, { like: true })
method: 'POST', .then(res => {
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({ like: true })
}).then(
res => res.json()
).then(function (res) {
if (res.error) { if (res.error) {
console.error({ error: res.error }); console.error({ error: res.error });
} else { } else {
this.increaseRevision(res.revision_id); this.increaseRevision(res.revision_id);
} }
}.bind(this)).catch( }).catch(
(err) => console.error({ error: err }) (err) => console.error({ error: err })
); );
} }
updateBuilding(buildingId, data) { updateBuilding(buildingId, data) {
fetch(`/api/buildings/${buildingId}.json`, { apiPost(`/api/buildings/${buildingId}.json`, data)
method: 'POST', .then(res => {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then(res => {
if (res.error) { if (res.error) {
console.error({ error: res.error }); console.error({ error: res.error });
} else { } else {

View File

@ -5,6 +5,7 @@ import { AttributionControl, GeoJSON, Map, TileLayer, ZoomControl } from 'react-
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import './map.css'; import './map.css';
import { apiGet } from '../apiHelpers';
import { HelpIcon } from '../components/icons'; import { HelpIcon } from '../components/icons';
import { Building } from '../models/building'; import { Building } from '../models/building';
@ -58,13 +59,9 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
handleClick(e) { handleClick(e) {
const mode = this.props.mode; const mode = this.props.mode;
const lat = e.latlng.lat; const { lat, lng } = e.latlng;
const lng = e.latlng.lng; apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`)
fetch( .then(data => {
'/api/buildings/locate?lat='+lat+'&lng='+lng
).then(
(res) => res.json()
).then(function(data){
if (data && data.length){ if (data && data.length){
const building = data[0]; const building = data[0];
if (mode === 'multi-edit') { if (mode === 'multi-edit') {
@ -82,7 +79,7 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
this.props.selectBuilding(undefined); this.props.selectBuilding(undefined);
} }
} }
}.bind(this)).catch( }).catch(
(err) => console.error(err) (err) => console.error(err)
); );
} }
@ -94,8 +91,8 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
} }
async getBoundary() { async getBoundary() {
const res = await fetch('/geometries/boundary-detailed.geojson'); const data = await apiGet('/geometries/boundary-detailed.geojson') as GeoJsonObject;
const data = await res.json() as GeoJsonObject;
this.setState({ this.setState({
boundary: data boundary: data
}); });

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import './search-box.css'; import './search-box.css';
import { apiGet } from '../apiHelpers';
import { SearchIcon } from '../components/icons'; import { SearchIcon } from '../components/icons';
interface SearchResult { interface SearchResult {
@ -96,11 +97,8 @@ class SearchBox extends Component<SearchBoxProps, SearchBoxState> {
fetching: true fetching: true
}); });
fetch( apiGet(`/api/search?q=${this.state.q}`)
'/api/search?q='+this.state.q .then((data) => {
).then(
(res) => res.json()
).then((data) => {
if (data && data.results){ if (data && data.results){
this.setState({ this.setState({
results: data.results, results: data.results,

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { apiGet } from '../apiHelpers';
import { BuildingEditSummary } from '../building/edit-history/building-edit-summary'; import { BuildingEditSummary } from '../building/edit-history/building-edit-summary';
import InfoBox from '../components/info-box'; import InfoBox from '../components/info-box';
import { EditHistoryEntry } from '../models/edit-history-entry'; import { EditHistoryEntry } from '../models/edit-history-entry';
@ -9,10 +10,9 @@ const ChangesPage = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const res = await fetch(`/api/history`); const {history} = await apiGet(`/api/history`);
const data = await res.json();
setHistory(data.history); setHistory(history);
}; };
fetchData(); fetchData();

View File

@ -2,6 +2,7 @@ import React, { FunctionComponent } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { dateReviver } from '../../helpers'; import { dateReviver } from '../../helpers';
import { apiGet } from '../apiHelpers';
interface ExtractViewModel { interface ExtractViewModel {
@ -28,11 +29,9 @@ export default class DataExtracts extends React.Component<{}, DataExtractsState>
} }
async componentDidMount() { async componentDidMount() {
const res = await fetch('/api/extracts'); let data = await apiGet('/api/extracts', { jsonReviver: dateReviver});
const data = JSON.parse(await res.text(), dateReviver);
const extracts = (data.extracts as ExtractViewModel[]) const extracts = (data.extracts as ExtractViewModel[])
.sort((a, b) => a.extracted_on.valueOf() - b.extracted_on.valueOf()) .sort((a, b) => b.extracted_on.valueOf() - a.extracted_on.valueOf());
.reverse();

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Link, Redirect } from 'react-router-dom'; import { Link, Redirect } from 'react-router-dom';
import { apiGet, apiPost } from '../apiHelpers';
import ErrorBox from '../components/error-box'; import ErrorBox from '../components/error-box';
import InfoBox from '../components/info-box'; import InfoBox from '../components/info-box';
import SupporterLogos from '../components/supporter-logos'; import SupporterLogos from '../components/supporter-logos';
@ -39,24 +40,13 @@ class Login extends Component<LoginProps, any> {
event.preventDefault(); event.preventDefault();
this.setState({error: undefined}); this.setState({error: undefined});
fetch('/api/login', { apiPost('/api/login', this.state)
method: 'POST', .then(res => {
body: JSON.stringify(this.state),
headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
if (res.error) { if (res.error) {
this.setState({error: res.error}); this.setState({error: res.error});
} else { } else {
fetch('/api/users/me', { apiGet('/api/users/me')
credentials: 'same-origin' .then(user => {
}).then(
(res) => res.json()
).then(user => {
if (user.error) { if (user.error) {
this.setState({error: user.error}); this.setState({error: user.error});
} else { } else {
@ -66,7 +56,7 @@ class Login extends Component<LoginProps, any> {
(err) => this.setState({error: err}) (err) => this.setState({error: err})
); );
} }
}.bind(this)).catch( }).catch(
(err) => this.setState({error: err}) (err) => this.setState({error: err})
); );
} }

View File

@ -1,6 +1,7 @@
import React, { Component, FormEvent } from 'react'; import React, { Component, FormEvent } from 'react';
import { Link, Redirect } from 'react-router-dom'; import { Link, Redirect } from 'react-router-dom';
import { apiDelete, apiPost } from '../apiHelpers';
import ConfirmationModal from '../components/confirmation-modal'; import ConfirmationModal from '../components/confirmation-modal';
import ErrorBox from '../components/error-box'; import ErrorBox from '../components/error-box';
import { User } from '../models/user'; import { User } from '../models/user';
@ -32,12 +33,8 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
event.preventDefault(); event.preventDefault();
this.setState({error: undefined}); this.setState({error: undefined});
fetch('/api/logout', { apiPost('/api/logout')
method: 'POST', .then(function(res){
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
if (res.error) { if (res.error) {
this.setState({error: res.error}); this.setState({error: res.error});
} else { } else {
@ -52,18 +49,14 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
event.preventDefault(); event.preventDefault();
this.setState({error: undefined}); this.setState({error: undefined});
fetch('/api/api/key', { apiPost('/api/api/key')
method: 'POST', .then(res => {
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
if (res.error) { if (res.error) {
this.setState({error: res.error}); this.setState({error: res.error});
} else { } else {
this.props.updateUser(res); this.props.updateUser(res);
} }
}.bind(this)).catch( }).catch(
(err) => this.setState({error: err}) (err) => this.setState({error: err})
); );
} }
@ -81,11 +74,7 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
this.setState({ error: undefined }); this.setState({ error: undefined });
try { try {
const res = await fetch('/api/users/me', { const data = await apiDelete('/api/users/me');
method: 'DELETE',
credentials: 'same-origin'
});
const data = await res.json();
if(data.error) { if(data.error) {
this.setState({ error: data.error }); this.setState({ error: data.error });

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Link, Redirect } from 'react-router-dom'; import { Link, Redirect } from 'react-router-dom';
import { apiGet, apiPost } from '../apiHelpers';
import ErrorBox from '../components/error-box'; import ErrorBox from '../components/error-box';
import InfoBox from '../components/info-box'; import InfoBox from '../components/info-box';
import SupporterLogos from '../components/supporter-logos'; import SupporterLogos from '../components/supporter-logos';
@ -48,36 +49,21 @@ class SignUp extends Component<SignUpProps, SignUpState> {
} as Pick<SignUpState, keyof SignUpState>); } as Pick<SignUpState, keyof SignUpState>);
} }
handleSubmit(event) { async handleSubmit(event) {
event.preventDefault(); event.preventDefault();
this.setState({error: undefined}); this.setState({error: undefined});
fetch('/api/users', { try {
method: 'POST', const res = await apiPost('/api/users', this.state);
body: JSON.stringify(this.state),
headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
if(res.error) { if(res.error) {
this.setState({ error: res.error }); this.setState({ error: res.error });
} else { } else {
fetch('/api/users/me', { const user = await apiGet('/api/users/me');
credentials: 'same-origin' this.props.login(user);
}).then( }
(res) => res.json() } catch(err) {
).then( this.setState({error: err});
(user) => this.props.login(user)
).catch(
(err) => this.setState({error: err})
);
} }
}.bind(this)).catch(
(err) => this.setState({error: err})
);
} }
render() { render() {