generic functions for backend queries

This commit is contained in:
Aarni Halinen
2021-09-04 18:02:29 +03:00
parent d48c6a0c3e
commit efd916a8a2
13 changed files with 200 additions and 230 deletions
+122
View File
@@ -0,0 +1,122 @@
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { getTokenCookie } from "@utils/auth";
export const HOST = `${process.env.NEXT_PUBLIC_API_URL}`;
const axiosInstance: AxiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});
export enum APIPath {
TAGS = "/tags/:id",
EVENTS = "/events/:id",
FEED = "/feed/:id",
JOBADS = "/jobads/:id",
SIGNUPS = "/signup/:id",
SIGNUP_FORMS = "/signupForm/:id",
AUTH_TOKEN = "/api-token-auth",
AUTH_TOKEN_VERIFY = "/api-token-verify",
}
export type API = {
path: APIPath;
urlParams?: {
id?: string | number;
};
queryParams?: {
limit?: number;
offset?: number;
since?: Date;
};
authenticated?: boolean;
};
type Headers = {
Authorization?: string;
};
const getAuthHeader = (): string => {
const jwt = getTokenCookie();
return `JWT ${jwt}`;
};
const getHeaders = (auth?: boolean): Headers => {
if (auth) {
return {
Authorization: getAuthHeader(),
};
}
return {};
};
const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => {
const path = apiPath
.split("/")
.filter(Boolean)
.map((urlComponent) => {
// fill in each placeholder component like ':id' with value from params
if (urlComponent.startsWith(":")) {
const key = urlComponent.substring(1);
const value = params[key];
return value;
}
return urlComponent;
})
.join("/");
// code above strips leading '/' from path
return `/${path}`;
};
async function callBackendAPI<RequestType, ResponseType>(
path: APIPath,
urlParams: API["urlParams"] = {},
queryParams: API["queryParams"] = {},
method: AxiosRequestConfig["method"],
headers: Headers,
requestBody: RequestType,
): Promise<ResponseType> {
const url = fillUrlParams(path, urlParams);
const request: AxiosRequestConfig = {
url,
method,
headers,
params: queryParams,
data: requestBody,
responseType: "json",
};
const response = await axiosInstance.request<ResponseType>(request);
const arrayResp = (response.data as { results?: ResponseType });
if (Array.isArray(arrayResp.results)) {
return arrayResp.results;
}
return response.data;
}
export async function getBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "GET", headers, undefined);
}
export async function postBackendAPI<RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}: API, body: RequestType): Promise<ResponseType> {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "POST", headers, body);
}
export async function putBackendAPI<RequestType, ResponseType>({
path, urlParams, queryParams, authenticated,
}: API, body: RequestType): Promise<ResponseType> {
const headers = getHeaders(authenticated);
return callBackendAPI<RequestType, ResponseType>(path, urlParams, queryParams, "PUT", headers, body);
}
export async function deleteBackendAPI<ResponseType>({
path, urlParams, queryParams, authenticated,
}: API): Promise<ResponseType> {
const headers = getHeaders(authenticated);
return callBackendAPI<undefined, ResponseType>(path, urlParams, queryParams, "DELETE", headers, undefined);
}
+25 -43
View File
@@ -1,11 +1,10 @@
/* eslint-disable no-console */
import axios from "axios";
import Event from "@models/Event";
import { getAuthHeader } from "@utils/auth";
import {
APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI,
} from "./backend";
export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`;
export interface Options {
interface Options {
limit?: number;
offset?: number;
auth?: boolean;
@@ -15,33 +14,28 @@ export interface Options {
class EventApi {
static async getEvent(id: number, auth = false): Promise<Event> {
try {
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}${id}/`, {
headers,
return await getBackendAPI({
path: APIPath.EVENTS, urlParams: { id }, authenticated: auth,
});
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
static async getEvents(options: Options = {}): Promise<Event[]> {
const {
since, limit, offset, auth,
} = options;
static async getEvents({
since, limit, offset, auth,
}: Options = {}): Promise<Event[]> {
try {
const params = {
since,
limit,
offset,
};
const headers = auth ? { Authorization: getAuthHeader() } : null;
const resp = await axios.get(`${URL}`, {
headers,
params,
return await getBackendAPI<Event[]>({
path: APIPath.EVENTS,
queryParams: {
since,
limit,
offset,
},
authenticated: auth,
});
return resp.data.results;
} catch (err) {
console.error(err);
throw err;
@@ -50,12 +44,9 @@ class EventApi {
static async createEvent(data: Event): Promise<Event> {
try {
const resp = await axios.post(URL, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await postBackendAPI<Event, Event>({
path: APIPath.EVENTS, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
@@ -64,27 +55,18 @@ class EventApi {
static async updateEvent(data: Event): Promise<Event> {
try {
const putUrl = `${URL}${data.id}/`;
const resp = await axios.put(putUrl, data, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
return await putBackendAPI<Event, Event>({
path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true,
}, data);
} catch (err) {
console.error(err);
throw err;
}
}
static async deleteEvent(id: number) {
static async deleteEvent(id: number): Promise<void> {
try {
const resp = await axios.delete(`${URL}${id}`, {
headers: {
Authorization: getAuthHeader(),
},
});
return resp.data;
await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id } });
} catch (err) {
console.error(err);
throw err;
+27
View File
@@ -0,0 +1,27 @@
import useSWR from "swr";
import { APIPath, getBackendAPI } from "@api/backend";
function useFetchBackend<DataType>({
apiPath: path,
fallbackData,
options,
}: {
apiPath: APIPath,
fallbackData?: DataType,
options?: {
limit?: number;
auth?: boolean;
}
}): {
data?: DataType,
error?: any
} {
const fetcher = (limit: number, authenticated: boolean) => getBackendAPI<DataType>({ path, queryParams: { limit }, authenticated });
const { data, error } = useSWR([options?.limit, options?.auth], fetcher, { fallbackData });
return {
data,
error,
};
}
export default useFetchBackend;
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Event from "@models/Event";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/eventApi";
const fetcher = (url: string, config: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
auth, since, limit, offset,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: Event | Event[],
id?: string;
options?: Options
}
const useFetchEvents = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], fetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchEvents;
-53
View File
@@ -1,53 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import Post from "@models/Feed";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/feedApi";
const feedFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const { auth, limit, offset } = options;
return {
url,
config: {
params: {
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: Post | Post[],
id?: string;
options?: Options
}
const useFetchFeed = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], feedFetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchFeed;
-56
View File
@@ -1,56 +0,0 @@
import { useRef } from "react";
import useSWR from "swr";
import isDeepEqual from "fast-deep-equal/react";
import axios, { AxiosRequestConfig } from "axios";
import JobAd from "@models/JobAd";
import { getAuthHeader } from "@utils/auth";
import { URL, Options } from "@api/jobAdApi";
const jobAdFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data);
const generateFetchParams = (id = "", options: Options = {}) => {
const url = `${URL}${id}`;
const {
since, limit, offset, auth,
} = options;
return {
url,
config: {
params: {
since,
limit,
offset,
},
headers: auth ? { Authorization: getAuthHeader() } : null,
},
};
};
interface FetchArguments {
fallbackData?: JobAd | JobAd[],
id?: string;
options?: Options;
}
const useFetchJobAds = ({
fallbackData,
id = "",
options = {},
}: FetchArguments) => {
const { url, config } = generateFetchParams(id, options);
// Use ref, since config dependency is non-primitive => without this we have infinite fetch loop
const configRef = useRef(config);
if (!isDeepEqual(configRef.current, config)) {
configRef.current = config;
}
const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { fallbackData });
return {
data: data?.results || data,
error,
};
};
export default useFetchJobAds;
+3 -2
View File
@@ -8,7 +8,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import useFetchBackend from "@hooks/useFetchBackend";
import { APIPath } from "@api/backend";
const URL = "/admin/events";
@@ -65,7 +66,7 @@ const renderData = (events: Event[]) => {
};
const AdminEventPage: NextPage = () => {
const { data } = useFetchEvents({ options: { auth: true } });
const { data } = useFetchBackend<Event[]>({ apiPath: APIPath.EVENTS, options: { auth: true } });
return (
<AdminListCommon>
<h1>Events</h1>
+3 -2
View File
@@ -8,7 +8,8 @@ import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import Post from "@models/Feed";
import PostApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import useFetchBackend from "@hooks/useFetchBackend";
import { APIPath } from "@api/backend";
const URL = "/admin/feed";
@@ -65,7 +66,7 @@ const renderData = (feed: Post[]) => {
};
const AdminFeedPage: NextPage = () => {
const { data } = useFetchFeed({ options: { auth: true } });
const { data } = useFetchBackend<Post[]>({ apiPath: APIPath.FEED, options: { auth: true } });
return (
<AdminListCommon>
<h1>Feed</h1>
+3 -2
View File
@@ -7,8 +7,9 @@ import AdminListCommon from "@views/admin/AdminListCommon";
import { Button, Link } from "@components/index";
import AddLink from "@components/AddLink";
import JobAd from "@models/JobAd";
import useFetchJobAds from "@hooks/useFetchJobAds";
import useFetchBackend from "@hooks/useFetchBackend";
import JobAdApi from "@api/jobAdApi";
import { APIPath } from "@api/backend";
const URL = "/admin/jobads";
@@ -69,7 +70,7 @@ const renderData = (jobAds: JobAd[]) => {
};
const AdminJobAdPage: NextPage = () => {
const { data } = useFetchJobAds({ options: { auth: true } });
const { data } = useFetchBackend<JobAd[]>({ apiPath: APIPath.JOBADS, options: { auth: true } });
return (
<AdminListCommon>
<h1>Job advertisements</h1>
+5 -5
View File
@@ -3,12 +3,12 @@ import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import useFetchBackend from "@hooks/useFetchBackend";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import InEnglishPageView from "@views/InEnglishPage/InEnglishPageView";
import PageWrapper from "@views/common/PageWrapper";
import { APIPath } from "@api/backend";
const eventOptions = {
limit: 4,
@@ -24,8 +24,8 @@ interface InitialProps {
}
const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
const eventResult = useFetchBackend<Event[]>({ apiPath: APIPath.EVENTS, fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchBackend<Post[]>({ apiPath: APIPath.FEED, fallbackData: initialFeed, options: feedOptions });
return (
<>
@@ -33,7 +33,7 @@ const InEnglishPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) =
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/in_english`} />
</Head>
<PageWrapper>
<InEnglishPageView events={eventResult.data as Event[]} feed={feedResult.data} />
<InEnglishPageView events={eventResult.data} feed={feedResult.data} />
</PageWrapper>
</>
);
+5 -5
View File
@@ -3,12 +3,12 @@ import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import useFetchBackend from "@hooks/useFetchBackend";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import FrontPageView from "@views/FrontPage/FrontPageView";
import PageWrapper from "@views/common/PageWrapper";
import { APIPath } from "@api/backend";
const eventOptions = {
limit: 4,
@@ -24,8 +24,8 @@ interface InitialProps {
}
const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions });
const eventResult = useFetchBackend<Event[]>({ apiPath: APIPath.EVENTS, fallbackData: initialEvents, options: eventOptions });
const feedResult = useFetchBackend<Post[]>({ apiPath: APIPath.FEED, fallbackData: initialFeed, options: feedOptions });
return (
<>
@@ -33,7 +33,7 @@ const FrontPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
<link rel="canonical" href={`${process.env.NEXT_PUBLIC_SITE_URL}/`} />
</Head>
<PageWrapper>
<FrontPageView events={eventResult.data as Event[]} feed={feedResult.data} />
<FrontPageView events={eventResult.data} feed={feedResult.data} />
</PageWrapper>
</>
);
+4 -4
View File
@@ -3,12 +3,12 @@ import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import Event from "@models/Event";
import EventApi from "@api/eventApi";
import useFetchEvents from "@hooks/useFetchEvents";
import useFetchBackend from "@hooks/useFetchBackend";
import Post from "@models/Feed";
import FeedApi from "@api/feedApi";
import useFetchFeed from "@hooks/useFetchFeed";
import ActualPageView from "@views/ActualPage/ActualPageView";
import PageWrapper from "@views/common/PageWrapper";
import { APIPath } from "@api/backend";
interface InitialProps {
initialEvents: Event[];
@@ -16,8 +16,8 @@ interface InitialProps {
}
const ActualPage: NextPage<InitialProps> = ({ initialEvents, initialFeed }) => {
const eventResult = useFetchEvents({ fallbackData: initialEvents });
const feedResult = useFetchFeed({ fallbackData: initialFeed });
const eventResult = useFetchBackend<Event[]>({ apiPath: APIPath.EVENTS, fallbackData: initialEvents });
const feedResult = useFetchBackend<Post[]>({ apiPath: APIPath.FEED, fallbackData: initialFeed });
return (
<>
+3 -2
View File
@@ -3,9 +3,10 @@ import { NextPage, GetStaticProps } from "next";
import Head from "next/head";
import JobAd from "@models/JobAd";
import JobAdApi from "@api/jobAdApi";
import useFetchJobAds from "@hooks/useFetchJobAds";
import useFetchBackend from "@hooks/useFetchBackend";
import CorporatePageView from "@views/CorporatePage/CorporatePageView";
import PageWrapper from "@views/common/PageWrapper";
import { APIPath } from "@api/backend";
interface InitialProps {
initialJobAds: JobAd[];
@@ -13,7 +14,7 @@ interface InitialProps {
const CorporatePage: NextPage<InitialProps> = ({ initialJobAds }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data, error } = useFetchJobAds({ fallbackData: initialJobAds });
const { data, error } = useFetchBackend<JobAd[]>({ apiPath: APIPath.JOBADS, fallbackData: initialJobAds });
return (
<>
<Head>