standardize api

This commit is contained in:
Michael Fatemi 2021-07-02 23:28:17 -04:00
parent 179047d672
commit 7e27f35b0a
15 changed files with 89 additions and 288 deletions

View File

@ -1,43 +0,0 @@
import axios from 'axios';
import { APIError } from './error';
import { makeAPIGetCall } from './utils';
// eslint-disable-next-line
const dev = process.env.NODE_ENV === 'development';
export const API_ENDPOINT = 'http://localhost:5000/api';
export const ION_AUTHORIZATION_ENDPOINT = dev
? 'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fion%2Fcallback'
: 'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=https%3A%2F%2Fwheelshare.space%2Fauth%2Fion%2Fcallback';
axios.defaults.baseURL = API_ENDPOINT;
/**
*
* @param code The code sent by the OAuth provider
* @param provider The provider that send the OAuth code
*/
export async function createSession(
code: string,
provider: string
): Promise<string> {
const response = await axios.post('/auth/create_session', {
code,
provider,
});
if (response.status === 200) {
return response.data.session_id;
} else {
throw new APIError('/auth/create_session', response.data.error);
}
}
export async function getMe(): Promise<Carpool.User> {
let result = await makeAPIGetCall('/users/@me');
return result.data.data;
}
export async function getPublicUser(id: string): Promise<Carpool.PublicUser> {
let result = await makeAPIGetCall(`/users/${id}`);
return result.data.data;
}

View File

@ -1,5 +0,0 @@
export class APIError extends Error {
constructor(route: string, message: string) {
super('API Error @' + route + ': ' + message);
}
}

View File

@ -1,19 +0,0 @@
export type PlaceDetails = {
formattedAddress: string;
latitude: number;
longitude: number;
};
export async function getPlaceDetails(
placeId: string
): Promise<PlaceDetails | null> {
if (placeId == null) {
console.warn('placeId was null');
return null;
}
const result = await fetch('http://localhost:5000/api/place/' + placeId);
const json = await result.json();
return json;
}

View File

@ -1,100 +0,0 @@
import axios, { AxiosResponse } from 'axios';
import getSessionId from '../lib/getSessionId';
import { APIError } from './error';
export function createUrlWithGetParameters(
url: string,
params?: Record<string, string | undefined>
) {
if (params) {
// Stringify the parameters
let stringifiedParameters = '';
for (let [name, value] of Object.entries(params)) {
if (value) {
stringifiedParameters +=
encodeURIComponent(name) + '=' + encodeURIComponent(value);
}
}
if (stringifiedParameters) {
url += '?' + stringifiedParameters;
}
}
return url;
}
export function graphql(query: string, variables?: any) {
return new Promise<AxiosResponse>((resolve, reject) => {
axios
.post(
'/graphql',
{ query, variables },
{ headers: { Authorization: 'Bearer ' + getSessionId() } }
)
.then((successfulResponse) => {
resolve(successfulResponse.data.data);
})
.catch((error) => {
if (error.response.status === 401) {
localStorage.removeItem('session_token');
window.location.pathname = '/';
} else {
reject(new APIError('/api/users/@me', error.response.data.error));
}
});
});
}
export function makeAPIPostCall(url: string, data?: any) {
return new Promise<AxiosResponse>((resolve, reject) => {
axios
.post(url, data, {
headers: { Authorization: 'Bearer ' + getSessionId() },
})
.then((successfulResponse) => resolve(successfulResponse))
.catch((error) => {
if (error.response?.status === 401) {
localStorage.removeItem('session_token');
window.location.pathname = '/';
} else {
reject(new APIError(url, error.response?.data?.error));
}
});
});
}
export function makeAPIGetCall(
url: string,
params?: Record<string, string | undefined>
) {
url = createUrlWithGetParameters(url, params);
return new Promise<AxiosResponse>((resolve, reject) => {
axios
.get(url, {
headers: { Authorization: 'Bearer ' + getSessionId() },
})
.then((successfulResponse) => resolve(successfulResponse))
.catch((error) => {
if (error.response?.status === 401) {
localStorage.removeItem('session_token');
window.location.pathname = '/';
} else {
reject(error);
}
});
});
}
export function makeAPIDeleteCall(url: string) {
return new Promise<AxiosResponse>((resolve, reject) => {
axios
.delete(url, { headers: { Authorization: 'Bearer ' + getSessionId() } })
.then((successfulResponse) => resolve(successfulResponse))
.catch((error) => {
if (error.response?.status === 401) {
localStorage.removeItem('session_token');
} else {
reject(new APIError(url, error.response.data.error));
}
});
});
}

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { getMe } from '../../api/api'; import { getMe } from '../api';
import AuthenticationContext, { AuthState } from './AuthenticationContext'; import AuthenticationContext, { AuthState } from './AuthenticationContext';
export default function AuthenticationWrapper({ export default function AuthenticationWrapper({

View File

@ -1,6 +1,7 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { Redirect, useLocation, useParams } from 'react-router-dom'; import { Redirect, useLocation, useParams } from 'react-router-dom';
import AuthenticationContext from './AuthenticationContext'; import AuthenticationContext from './AuthenticationContext';
import { createSession } from './createSession';
export default function Authenticator() { export default function Authenticator() {
const { provider } = useParams<{ provider: string }>(); const { provider } = useParams<{ provider: string }>();
@ -11,14 +12,7 @@ export default function Authenticator() {
useState<'pending' | 'errored' | 'authenticated'>('pending'); useState<'pending' | 'errored' | 'authenticated'>('pending');
useEffect(() => { useEffect(() => {
fetch('http://localhost:5000/create_session', { createSession(code!)
method: 'post',
body: JSON.stringify({ code }),
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((data) => { .then((data) => {
if (data.status === 'success') { if (data.status === 'success') {
localStorage.setItem('session_token', data.token); localStorage.setItem('session_token', data.token);

View File

@ -0,0 +1,16 @@
export async function createSession(code: string) {
const res = await fetch('http://localhost:5000/create_session', {
method: 'post',
body: JSON.stringify({ code }),
headers: {
'Content-Type': 'application/json',
},
});
const json = await res.json();
return {
status: json.status,
token: json.token,
};
}

View File

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { post } from './api'; import { post, removeEventSignup } from './api';
import { green, lightgrey } from './colors'; import { green, lightgrey } from './colors';
import latlongdist, { R_miles } from './latlongdist'; import latlongdist, { R_miles } from './latlongdist';
import UIButton from './UIButton'; import UIButton from './UIButton';
@ -306,9 +306,7 @@ export default function Event({ event }: { event: IEvent }) {
(prev.interested === true && interested === false) || (prev.interested === true && interested === false) ||
(interested === true && prev.placeId !== null && placeId === null) (interested === true && prev.placeId !== null && placeId === null)
) { ) {
fetch(`http://localhost:5000/api/events/${event.id}/signup`, { removeEventSignup(event.id).finally(() => setUpdating(false));
method: 'delete',
}).finally(() => setUpdating(false));
prev.interested = false; prev.interested = false;
return; return;
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getEvents } from './api';
import { IEvent } from './Event'; import { IEvent } from './Event';
import EventStream from './EventStream'; import EventStream from './EventStream';
@ -6,9 +7,7 @@ export default function Events() {
const [events, setEvents] = useState<IEvent[]>([]); const [events, setEvents] = useState<IEvent[]>([]);
useEffect(() => { useEffect(() => {
fetch('http://localhost:5000/api/events') getEvents().then(setEvents);
.then((res) => res.json())
.then(setEvents);
}, []); }, []);
return ( return (

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getGroup, getGroupEvents } from './api';
import { IEvent } from './Event'; import { IEvent } from './Event';
import EventCreatorLink from './EventCreatorLink'; import EventCreatorLink from './EventCreatorLink';
import EventStream from './EventStream'; import EventStream from './EventStream';
@ -21,14 +22,11 @@ export default function Group() {
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
fetch('http://localhost:5000/api/groups/' + id) getGroup(+id)
.then((response) => response.json())
.then(setGroup) .then(setGroup)
.finally(() => setLoading(false)); .finally(() => setLoading(false));
fetch('http://localhost:5000/api/groups/' + id + '/events') getGroupEvents(+id).then(setEvents);
.then((response) => response.json())
.then(setEvents);
}, [id]); }, [id]);
if (!group && !loading) { if (!group && !loading) {

View File

@ -1,4 +1,5 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { deleteGroup } from './api';
import { IGroup } from './Group'; import { IGroup } from './Group';
import UILink from './UILink'; import UILink from './UILink';
import UIPressable from './UIPressable'; import UIPressable from './UIPressable';
@ -9,9 +10,8 @@ function GroupSettings({ group }: { group: IGroup }) {
const [deletionSuccessful, setDeletionSuccessful] = const [deletionSuccessful, setDeletionSuccessful] =
useState<boolean | null>(null); useState<boolean | null>(null);
const deleteGroup = useCallback(() => { const onClickedDelete = useCallback(() => {
fetch('http://localhost:5000/api/groups/' + group.id, { method: 'delete' }) deleteGroup(group.id)
.then((res) => res.json())
.then(({ status }) => { .then(({ status }) => {
setDeletionSuccessful(status === 'success'); setDeletionSuccessful(status === 'success');
}) })
@ -24,7 +24,7 @@ function GroupSettings({ group }: { group: IGroup }) {
<UISecondaryBox> <UISecondaryBox>
<h1>Settings</h1> <h1>Settings</h1>
{deletionSuccessful !== true && ( {deletionSuccessful !== true && (
<UIPressable onClick={deleteGroup}>Delete Group</UIPressable> <UIPressable onClick={onClickedDelete}>Delete Group</UIPressable>
)} )}
{deletionSuccessful !== null && {deletionSuccessful !== null &&
(deletionSuccessful ? ( (deletionSuccessful ? (

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getGroups } from './api';
import { IGroup } from './Group'; import { IGroup } from './Group';
import GroupCreatorLink from './GroupCreatorLink'; import GroupCreatorLink from './GroupCreatorLink';
import GroupJoinerLink from './GroupJoinerLink'; import GroupJoinerLink from './GroupJoinerLink';
@ -8,9 +9,7 @@ export default function Groups() {
const [groups, setGroups] = useState<IGroup[]>([]); const [groups, setGroups] = useState<IGroup[]>([]);
useEffect(() => { useEffect(() => {
fetch('http://localhost:5000/api/groups') getGroups().then(setGroups);
.then((res) => res.json())
.then(setGroups);
}, []); }, []);
return ( return (

View File

@ -1,18 +1,70 @@
export function post(path: string, data: any) { export async function post(path: string, data: any) {
return fetch('http://localhost:5000/api' + path, { const res = await fetch('http://localhost:5000/api' + path, {
method: 'post', method: 'post',
body: JSON.stringify(data), body: JSON.stringify(data),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
return await res.json();
} }
export function delete$(path: string, data: any) { export async function delete$(path: string) {
return fetch('http://localhost:5000/api' + path, { const res = await fetch('http://localhost:5000/api' + path, {
method: 'delete', method: 'delete',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
return await res.json();
}
export async function get(path: string) {
const res = await fetch('http://localhost:5000/api' + path);
return await res.json();
}
export type PlaceDetails = {
formattedAddress: string;
latitude: number;
longitude: number;
};
export async function getPlaceDetails(
placeId: string
): Promise<PlaceDetails | null> {
if (placeId == null) {
console.warn('placeId was null');
return null;
}
return await get('/place/' + placeId);
}
export async function removeEventSignup(eventId: number) {
return await delete$(`/events/${eventId}/signup`);
}
export async function getEvents() {
return await get('/events');
}
export async function getGroup(id: number) {
return await get('/groups/' + id);
}
export async function getGroupEvents(id: number) {
return await get('/groups/' + id + '/events');
}
export async function getGroups() {
return await get('/groups');
}
export async function deleteGroup(id: number) {
return await delete$('/groups/' + id);
}
export async function getMe() {
return await get('/users/@me');
} }

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { getPlaceDetails, PlaceDetails } from '../api/google'; import { getPlaceDetails, PlaceDetails } from './api';
import useThrottle from './useThrottle'; import useThrottle from './useThrottle';
export default function usePlace(placeId: string | null) { export default function usePlace(placeId: string | null) {

View File

@ -1,88 +0,0 @@
import { useEffect, useState } from 'react';
export type UpdateListener<T> = (newValue: StoreValue<T> | null) => void;
export type ValueFetcher<T> = (key: string) => Promise<T | null>;
export type StoreValue<T> = { value: T } | { error: any };
/**
* This is a general-purpose, subscribable key-value store for content like posts, groups, and spaces.
*/
export class Store<T> {
/**
* Stores the internal data. If the value is `null`, then we attempted to fetch the data, but it did not exist.
*/
private data = new Map<string, StoreValue<T> | null>();
private listeners = new Map<string, Set<UpdateListener<T>>>();
constructor(private fetcher: ValueFetcher<T>) {}
/**
*
* @param key The key to get the data for
* @param forceRefresh If the data already exists, fetch it again anyway
*/
get(key: string, forceRefresh = false): StoreValue<T> | null {
if (!this.data.has(key) || forceRefresh) {
this.fetcher(key)
.then((value) => {
this.set(key, value ? { value } : null);
})
.catch((error) => {
this.set(key, { error });
});
return null;
}
return this.data.get(key) ?? null;
}
set(key: string, value: StoreValue<T> | null) {
if (this.listeners.has(key)) {
this.listeners.get(key)?.forEach((callback) => {
callback(value);
});
}
return this.data.set(key, value);
}
subscribe(key: string, listener: UpdateListener<T>) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key)?.add(listener);
}
unsubscribe(key: string, listener: UpdateListener<T>) {
if (this.listeners.has(key)) {
if (this.listeners.get(key)?.has(listener)) {
this.listeners.get(key)?.delete(listener);
return;
}
}
console.warn(
'Unsubscribed from',
key,
'but listener does not exist: ',
listener
);
}
}
export function useStoredValue<T>(store: Store<T>, key: string) {
const [value, setValue] = useState<StoreValue<T> | null>(store.get(key));
useEffect(() => {
const callback = (value: StoreValue<T> | null) => setValue(value);
store.subscribe(key, callback);
return () => store.unsubscribe(key, callback);
}, [key, store]);
return value;
}