From 7e27f35b0a36d7c1de5fb67969df8b65f8a838f7 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Fri, 2 Jul 2021 23:28:17 -0400 Subject: [PATCH] standardize api --- src/api/api.ts | 43 -------- src/api/error.ts | 5 - src/api/google.ts | 19 ---- src/api/utils.ts | 100 ------------------ .../Authentication/AuthenticationWrapper.tsx | 2 +- .../Authentication/Authenticator.tsx | 10 +- .../Authentication/createSession.ts | 16 +++ src/components/Event.tsx | 6 +- src/components/Events.tsx | 5 +- src/components/Group.tsx | 8 +- src/components/GroupSettingsLink.tsx | 8 +- src/components/Groups.tsx | 5 +- src/components/api.ts | 60 ++++++++++- src/components/usePlace.ts | 2 +- src/store.ts | 88 --------------- 15 files changed, 89 insertions(+), 288 deletions(-) delete mode 100644 src/api/api.ts delete mode 100644 src/api/error.ts delete mode 100644 src/api/google.ts delete mode 100644 src/api/utils.ts create mode 100644 src/components/Authentication/createSession.ts delete mode 100644 src/store.ts diff --git a/src/api/api.ts b/src/api/api.ts deleted file mode 100644 index ec68a36..0000000 --- a/src/api/api.ts +++ /dev/null @@ -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 { - 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 { - let result = await makeAPIGetCall('/users/@me'); - return result.data.data; -} - -export async function getPublicUser(id: string): Promise { - let result = await makeAPIGetCall(`/users/${id}`); - return result.data.data; -} diff --git a/src/api/error.ts b/src/api/error.ts deleted file mode 100644 index 40815f7..0000000 --- a/src/api/error.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class APIError extends Error { - constructor(route: string, message: string) { - super('API Error @' + route + ': ' + message); - } -} diff --git a/src/api/google.ts b/src/api/google.ts deleted file mode 100644 index 60578aa..0000000 --- a/src/api/google.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type PlaceDetails = { - formattedAddress: string; - latitude: number; - longitude: number; -}; - -export async function getPlaceDetails( - placeId: string -): Promise { - 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; -} diff --git a/src/api/utils.ts b/src/api/utils.ts deleted file mode 100644 index 249708d..0000000 --- a/src/api/utils.ts +++ /dev/null @@ -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 -) { - 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((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((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 -) { - url = createUrlWithGetParameters(url, params); - return new Promise((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((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)); - } - }); - }); -} diff --git a/src/components/Authentication/AuthenticationWrapper.tsx b/src/components/Authentication/AuthenticationWrapper.tsx index 3f27c08..e0c7716 100644 --- a/src/components/Authentication/AuthenticationWrapper.tsx +++ b/src/components/Authentication/AuthenticationWrapper.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { getMe } from '../../api/api'; +import { getMe } from '../api'; import AuthenticationContext, { AuthState } from './AuthenticationContext'; export default function AuthenticationWrapper({ diff --git a/src/components/Authentication/Authenticator.tsx b/src/components/Authentication/Authenticator.tsx index 558b110..96e7ddf 100644 --- a/src/components/Authentication/Authenticator.tsx +++ b/src/components/Authentication/Authenticator.tsx @@ -1,6 +1,7 @@ import { useContext, useEffect, useState } from 'react'; import { Redirect, useLocation, useParams } from 'react-router-dom'; import AuthenticationContext from './AuthenticationContext'; +import { createSession } from './createSession'; export default function Authenticator() { const { provider } = useParams<{ provider: string }>(); @@ -11,14 +12,7 @@ export default function Authenticator() { useState<'pending' | 'errored' | 'authenticated'>('pending'); useEffect(() => { - fetch('http://localhost:5000/create_session', { - method: 'post', - body: JSON.stringify({ code }), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((response) => response.json()) + createSession(code!) .then((data) => { if (data.status === 'success') { localStorage.setItem('session_token', data.token); diff --git a/src/components/Authentication/createSession.ts b/src/components/Authentication/createSession.ts new file mode 100644 index 0000000..52fe274 --- /dev/null +++ b/src/components/Authentication/createSession.ts @@ -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, + }; +} diff --git a/src/components/Event.tsx b/src/components/Event.tsx index 3caef51..b02014a 100644 --- a/src/components/Event.tsx +++ b/src/components/Event.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { post } from './api'; +import { post, removeEventSignup } from './api'; import { green, lightgrey } from './colors'; import latlongdist, { R_miles } from './latlongdist'; import UIButton from './UIButton'; @@ -306,9 +306,7 @@ export default function Event({ event }: { event: IEvent }) { (prev.interested === true && interested === false) || (interested === true && prev.placeId !== null && placeId === null) ) { - fetch(`http://localhost:5000/api/events/${event.id}/signup`, { - method: 'delete', - }).finally(() => setUpdating(false)); + removeEventSignup(event.id).finally(() => setUpdating(false)); prev.interested = false; return; } diff --git a/src/components/Events.tsx b/src/components/Events.tsx index 633e691..136656a 100644 --- a/src/components/Events.tsx +++ b/src/components/Events.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { getEvents } from './api'; import { IEvent } from './Event'; import EventStream from './EventStream'; @@ -6,9 +7,7 @@ export default function Events() { const [events, setEvents] = useState([]); useEffect(() => { - fetch('http://localhost:5000/api/events') - .then((res) => res.json()) - .then(setEvents); + getEvents().then(setEvents); }, []); return ( diff --git a/src/components/Group.tsx b/src/components/Group.tsx index eed6e50..008e7eb 100644 --- a/src/components/Group.tsx +++ b/src/components/Group.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router'; import { Link } from 'react-router-dom'; +import { getGroup, getGroupEvents } from './api'; import { IEvent } from './Event'; import EventCreatorLink from './EventCreatorLink'; import EventStream from './EventStream'; @@ -21,14 +22,11 @@ export default function Group() { useEffect(() => { setLoading(true); - fetch('http://localhost:5000/api/groups/' + id) - .then((response) => response.json()) + getGroup(+id) .then(setGroup) .finally(() => setLoading(false)); - fetch('http://localhost:5000/api/groups/' + id + '/events') - .then((response) => response.json()) - .then(setEvents); + getGroupEvents(+id).then(setEvents); }, [id]); if (!group && !loading) { diff --git a/src/components/GroupSettingsLink.tsx b/src/components/GroupSettingsLink.tsx index e9f0daf..79cf264 100644 --- a/src/components/GroupSettingsLink.tsx +++ b/src/components/GroupSettingsLink.tsx @@ -1,4 +1,5 @@ import { useCallback, useState } from 'react'; +import { deleteGroup } from './api'; import { IGroup } from './Group'; import UILink from './UILink'; import UIPressable from './UIPressable'; @@ -9,9 +10,8 @@ function GroupSettings({ group }: { group: IGroup }) { const [deletionSuccessful, setDeletionSuccessful] = useState(null); - const deleteGroup = useCallback(() => { - fetch('http://localhost:5000/api/groups/' + group.id, { method: 'delete' }) - .then((res) => res.json()) + const onClickedDelete = useCallback(() => { + deleteGroup(group.id) .then(({ status }) => { setDeletionSuccessful(status === 'success'); }) @@ -24,7 +24,7 @@ function GroupSettings({ group }: { group: IGroup }) {

Settings

{deletionSuccessful !== true && ( - Delete Group + Delete Group )} {deletionSuccessful !== null && (deletionSuccessful ? ( diff --git a/src/components/Groups.tsx b/src/components/Groups.tsx index 149e172..ba7dd1e 100644 --- a/src/components/Groups.tsx +++ b/src/components/Groups.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { getGroups } from './api'; import { IGroup } from './Group'; import GroupCreatorLink from './GroupCreatorLink'; import GroupJoinerLink from './GroupJoinerLink'; @@ -8,9 +9,7 @@ export default function Groups() { const [groups, setGroups] = useState([]); useEffect(() => { - fetch('http://localhost:5000/api/groups') - .then((res) => res.json()) - .then(setGroups); + getGroups().then(setGroups); }, []); return ( diff --git a/src/components/api.ts b/src/components/api.ts index 10f5123..a9d04a0 100644 --- a/src/components/api.ts +++ b/src/components/api.ts @@ -1,18 +1,70 @@ -export function post(path: string, data: any) { - return fetch('http://localhost:5000/api' + path, { +export async function post(path: string, data: any) { + const res = await fetch('http://localhost:5000/api' + path, { method: 'post', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', }, }); + return await res.json(); } -export function delete$(path: string, data: any) { - return fetch('http://localhost:5000/api' + path, { +export async function delete$(path: string) { + const res = await fetch('http://localhost:5000/api' + path, { method: 'delete', headers: { '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 { + 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'); } diff --git a/src/components/usePlace.ts b/src/components/usePlace.ts index 1c73d14..b9a5a17 100644 --- a/src/components/usePlace.ts +++ b/src/components/usePlace.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { getPlaceDetails, PlaceDetails } from '../api/google'; +import { getPlaceDetails, PlaceDetails } from './api'; import useThrottle from './useThrottle'; export default function usePlace(placeId: string | null) { diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index 9479715..0000000 --- a/src/store.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect, useState } from 'react'; - -export type UpdateListener = (newValue: StoreValue | null) => void; - -export type ValueFetcher = (key: string) => Promise; - -export type StoreValue = { value: T } | { error: any }; - -/** - * This is a general-purpose, subscribable key-value store for content like posts, groups, and spaces. - */ -export class Store { - /** - * 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 | null>(); - private listeners = new Map>>(); - - constructor(private fetcher: ValueFetcher) {} - - /** - * - * @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 | 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 | 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) { - if (!this.listeners.has(key)) { - this.listeners.set(key, new Set()); - } - - this.listeners.get(key)?.add(listener); - } - - unsubscribe(key: string, listener: UpdateListener) { - 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(store: Store, key: string) { - const [value, setValue] = useState | null>(store.get(key)); - - useEffect(() => { - const callback = (value: StoreValue | null) => setValue(value); - store.subscribe(key, callback); - - return () => store.unsubscribe(key, callback); - }, [key, store]); - - return value; -}