setup api client

This commit is contained in:
Hayden 2022-08-30 16:06:57 -08:00
parent c780a0d3ac
commit 9c1cced576
8 changed files with 227 additions and 0 deletions

View file

@ -0,0 +1,17 @@
import { Requests } from '../../lib/requests';
// <
// TGetResult,
// TPostData,
// TPostResult,
// TPutData = TPostData,
// TPutResult = TPostResult,
// TDeleteResult = void
// >
export class BaseAPI {
http: Requests;
constructor(requests: Requests) {
this.http = requests;
}
}

View file

@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { UrlBuilder } from '.';
describe('UrlBuilder', () => {
it('basic query parameter', () => {
const result = UrlBuilder('/test', { a: 'b' });
expect(result).toBe('/api/v1/test?a=b');
});
it('multiple query parameters', () => {
const result = UrlBuilder('/test', { a: 'b', c: 'd' });
expect(result).toBe('/api/v1/test?a=b&c=d');
});
it('no query parameters', () => {
const result = UrlBuilder('/test');
expect(result).toBe('/api/v1/test');
});
it('list-like query parameters', () => {
const result = UrlBuilder('/test', { a: ['b', 'c'] });
expect(result).toBe('/api/v1/test?a=b&a=c');
});
});

View file

@ -0,0 +1,2 @@
export { BaseAPI } from './base-api';
export { UrlBuilder } from './urls';

View file

@ -0,0 +1,31 @@
export const prefix = '/api/v1';
export type QueryValue =
| string
| string[]
| number
| number[]
| boolean
| null
| undefined;
export function UrlBuilder(
rest: string,
params: Record<string, QueryValue> = {}
): string {
// we use a stub base URL to leverage the URL class
const url = new URL(prefix + rest, 'http://localhost.com');
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
for (const item of value) {
url.searchParams.append(key, String(item));
}
} else {
url.searchParams.append(key, String(value));
}
}
// we return the path only, without the base URL
return url.toString().replace('http://localhost.com', '');
}

View file

@ -0,0 +1,39 @@
import { BaseAPI, UrlBuilder } from './base';
export type LoginResult = {
token: string;
expiresAt: string;
};
export type LoginPayload = {
username: string;
password: string;
};
export type RegisterPayload = {
user: {
email: string;
password: string;
name: string;
};
groupName: string;
};
export class PublicApi extends BaseAPI {
public login(username: string, password: string) {
return this.http.post<LoginPayload, LoginResult>(
UrlBuilder('/users/login'),
{
username,
password,
}
);
}
public register(payload: RegisterPayload) {
return this.http.post<RegisterPayload, LoginResult>(
UrlBuilder('/users/register'),
payload
);
}
}

18
frontend/src/api/user.ts Normal file
View file

@ -0,0 +1,18 @@
import { BaseAPI, UrlBuilder } from './base';
export type Result<T> = {
item: T;
};
export type User = {
name: string;
email: string;
isSuperuser: boolean;
id: number;
};
export class UserApi extends BaseAPI {
public self() {
return this.http.get<Result<User>>(UrlBuilder('/users/self'));
}
}

View file

@ -0,0 +1 @@
export { Requests, type TResponse } from './requests';

View file

@ -0,0 +1,95 @@
export enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
}
export interface TResponse<T> {
status: number;
error: boolean;
data: T;
response: Response;
}
export class Requests {
private baseUrl: string;
private token: () => string;
private headers: Record<string, string> = {};
private logger?: (response: Response) => void;
private url(rest: string): string {
return this.baseUrl + rest;
}
constructor(
baseUrl: string,
token: string | (() => string) = '',
headers: Record<string, string> = {},
logger?: (response: Response) => void
) {
this.baseUrl = baseUrl;
this.token = typeof token === 'string' ? () => token : token;
this.headers = headers;
this.logger = logger;
}
public get<T>(url: string): Promise<TResponse<T>> {
return this.do<T>(Method.GET, url);
}
public post<T, U>(url: string, payload: T): Promise<TResponse<U>> {
return this.do<U>(Method.POST, url, payload);
}
public put<T, U>(url: string, payload: T): Promise<TResponse<U>> {
return this.do<U>(Method.PUT, url, payload);
}
public delete<T>(url: string): Promise<TResponse<T>> {
return this.do<T>(Method.DELETE, url);
}
private methodSupportsBody(method: Method): boolean {
return method === Method.POST || method === Method.PUT;
}
private async do<T>(
method: Method,
url: string,
payload: Object = {}
): Promise<TResponse<T>> {
const args: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...this.headers,
},
};
const token = this.token();
if (token !== '' && args.headers !== undefined) {
// @ts-expect-error -- headers is always defined at this point
args.headers['Authorization'] = token;
}
if (this.methodSupportsBody(method)) {
args.body = JSON.stringify(payload);
}
const response = await fetch(this.url(url), args);
if (this.logger) {
this.logger(response);
}
const data = await response.json();
return {
status: response.status,
error: !response.ok,
data,
response,
};
}
}