Pass query throw args

This commit is contained in:
Pavel Mineev
2021-02-02 15:53:40 -06:00
committed by Umputun
parent 2fae0c29d2
commit 8161182e7d
3 changed files with 101 additions and 108 deletions
+40 -76
View File
@@ -1,41 +1,17 @@
import { siteId, url } from './settings';
import { BASE_URL, API_BASE } from './constants';
import { Config, Comment, Tree, User, BlockedUser, Sorting, AuthProvider, BlockTTL, Image } from './types';
import fetcher from './fetcher';
// /auth/anonymous/login?user=sfdfsf&aud=remark&from=https%3A%2F%2Fdemo.remark42.com%2Fweb%2Fiframe.html%3FselfClose&site=remark
// /auth/anonymous/login?site=remark&username=asdasd&aud=remark&from=http://127.0.0.1:9000/web/iframe.html?selfClose
function stringifyUrl(url: string, query: Record<string, string | number | undefined>) {
const queryEntries = Object.entries(query);
const siteIdParam = `site=${encodeURIComponent(siteId)}`;
if (queryEntries.length === 0) {
return `${url}?${siteIdParam}`;
}
const queryString = Object.entries(query)
.reduce(
(accum, [k, v]) => (v === undefined ? accum : [...accum, `${encodeURIComponent(k)}=${encodeURIComponent(v)}`]),
[siteIdParam] as string[]
)
.join('&');
return `${url}?${queryString}`;
}
import fetcher, { stringifyUrl } from './fetcher';
/* common */
const __loginAnonymously = (username: string): Promise<User | null> =>
fetcher.get<User>(
stringifyUrl('/auth/anonymous/login', {
user: username,
aud: siteId,
from: `${window.location.origin}${window.location.pathname}?selfClose`,
})
);
fetcher.get<User>('/auth/anonymous/login', {
user: username,
aud: siteId,
from: `${window.location.origin}${window.location.pathname}?selfClose`,
});
const __loginViaEmail = (token: string): Promise<User | null> =>
fetcher.get<User>(stringifyUrl('/auth/email/login', { token }));
const __loginViaEmail = (token: string): Promise<User | null> => fetcher.get<User>('/auth/email/login', { token });
/**
* First step of two of `email` authorization
@@ -44,14 +20,14 @@ const __loginViaEmail = (token: string): Promise<User | null> =>
* @param address email address
*/
export const sendEmailVerificationRequest = (username: string, address: string): Promise<void> =>
fetcher.get(stringifyUrl('/auth/email/login', { id: siteId, user: username, address }));
fetcher.get('/auth/email/login', { id: siteId, user: username, address });
export const logIn = (provider: AuthProvider): Promise<User | null> => {
if (provider.name === 'anonymous') return __loginAnonymously(provider.username);
if (provider.name === 'email') return __loginViaEmail(provider.token);
return new Promise<User | null>((resolve, reject) => {
const url = stringifyUrl(`${BASE_URL}/auth/${provider.name}/login`, {
const url = stringifyUrl(`/auth/${provider.name}/login`, {
from: `${window.location.origin}${window.location.pathname}?selfClose`,
});
const newWindow = window.open(url);
@@ -83,10 +59,9 @@ export const logOut = (): Promise<void> => fetcher.get('/auth/logout');
export const getConfig = (): Promise<Config> => fetcher.get('/config');
export const getPostComments = (sort: Sorting) =>
fetcher.get<Tree>(stringifyUrl('/find', { url, sort, format: 'tree' }));
export const getPostComments = (sort: Sorting) => fetcher.get<Tree>('/find', { url, sort, format: 'tree' });
export const getComment = (id: Comment['id']): Promise<Comment> => fetcher.get(stringifyUrl(`/id/${id}`, { url }));
export const getComment = (id: Comment['id']): Promise<Comment> => fetcher.get(`/id/${id}`, { url });
export const getUserComments = (
userId: User['id'],
@@ -94,10 +69,10 @@ export const getUserComments = (
): Promise<{
comments: Comment[];
count: number;
}> => fetcher.get(stringifyUrl('/comments', { user: userId, limit }));
}> => fetcher.get('/comments', { user: userId, limit });
export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise<void> =>
fetcher.put(stringifyUrl(`/vote/${id}`, { url, vote: value }));
fetcher.put(`/vote/${id}`, { url, vote: value });
export const addComment = ({
title,
@@ -108,19 +83,21 @@ export const addComment = ({
text: string;
pid?: Comment['id'];
}): Promise<Comment> =>
fetcher.post('/comment', {
json: {
fetcher.post(
'/comment',
{},
{
title,
text,
locator: { site: siteId, url },
...(pid ? { pid } : {}),
},
});
}
);
export const updateComment = ({ text, id }: { text: string; id: Comment['id'] }): Promise<Comment> =>
fetcher.put(stringifyUrl(`/comment/${id}`, { url }), { json: { text } });
fetcher.put(`/comment/${id}`, { url }, { text });
export const getPreview = (text: string): Promise<string> => fetcher.post('/preview', { json: { text } });
export const getPreview = (text: string): Promise<string> => fetcher.post('/preview', {}, { text });
export const getUser = (): Promise<User | null> => fetcher.get<User | null>('/user').catch(() => null);
@@ -128,27 +105,22 @@ export const getUser = (): Promise<User | null> => fetcher.get<User | null>('/us
export const deleteMe = (): Promise<{ user_id: string; link: string }> => fetcher.post('/deleteme');
export const approveDeleteMe = (token: string): Promise<void> =>
fetcher.get(stringifyUrl('/admin/deleteme', { token }));
export const approveDeleteMe = (token: string): Promise<void> => fetcher.get('/admin/deleteme', { token });
/* admin */
export const pinComment = (id: Comment['id']): Promise<void> =>
fetcher.put(stringifyUrl(`/admin/pin/${id}`, { url, pin: 1 }));
export const pinComment = (id: Comment['id']): Promise<void> => fetcher.put(`/admin/pin/${id}`, { url, pin: 1 });
export const unpinComment = (id: Comment['id']): Promise<void> =>
fetcher.put(stringifyUrl(`/admin/pin/${id}`, { url, pin: 0 }));
export const unpinComment = (id: Comment['id']): Promise<void> => fetcher.put(`/admin/pin/${id}`, { url, pin: 0 });
export const setVerifiedStatus = (id: User['id']): Promise<void> =>
fetcher.put(stringifyUrl(`/admin/verify/${id}`, { verified: 1 }));
export const setVerifiedStatus = (id: User['id']): Promise<void> => fetcher.put(`/admin/verify/${id}`, { verified: 1 });
export const removeVerifiedStatus = (id: User['id']): Promise<void> =>
fetcher.put(stringifyUrl(`/admin/verify/${id}`, { verified: 0 }));
fetcher.put(`/admin/verify/${id}`, { verified: 0 });
export const removeComment = (id: Comment['id']): Promise<void> =>
fetcher.delete(stringifyUrl(`/admin/comment/${id}`, { url }));
export const removeComment = (id: Comment['id']): Promise<void> => fetcher.delete(`/admin/comment/${id}`, { url });
export const removeMyComment = (id: Comment['id']): Promise<void> =>
fetcher.put(stringifyUrl(`/comment/${id}`, { url }), { json: { delete: true } });
fetcher.put(`/comment/${id}`, { url }, { delete: true });
export const blockUser = (
id: User['id'],
@@ -157,7 +129,7 @@ export const blockUser = (
block: boolean;
site_id: string;
user_id: string;
}> => fetcher.put(stringifyUrl(`/admin/user/${id}`, { block: 1, ttl: ttl === 'permanently' ? ttl : undefined }));
}> => fetcher.put(`/admin/user/${id}`, { block: 1, ttl: ttl === 'permanently' ? ttl : undefined });
export const unblockUser = (
id: User['id']
@@ -165,31 +137,24 @@ export const unblockUser = (
block: boolean;
site_id: string;
user_id: string;
}> => fetcher.put(stringifyUrl(`/admin/user/${id}`, { block: 0 }));
}> => fetcher.put(`/admin/user/${id}`, { block: 0 });
export const getBlocked = (): Promise<BlockedUser[] | null> => fetcher.get('/admin/blocked');
export const disableComments = (): Promise<void> => fetcher.put(stringifyUrl('/admin/readonly', { url, ro: 1 }));
export const disableComments = (): Promise<void> => fetcher.put('/admin/readonly', { url, ro: 1 });
export const enableComments = (): Promise<void> => fetcher.put(stringifyUrl('/admin/readonly', { url, ro: 0 }));
export const enableComments = (): Promise<void> => fetcher.put('/admin/readonly', { url, ro: 0 });
export const uploadImage = (image: File): Promise<Image> => {
const data = new FormData();
data.append('file', image);
return fetcher
.post<{ id: string }>('/picture', {
headers: {
'Content-Type': 'multipart/form-data',
},
body: data,
})
.then((resp) => ({
name: image.name,
size: image.size,
type: image.type,
url: `${BASE_URL + API_BASE}/picture/${resp.id}`,
}));
return fetcher.post<{ id: string }>('/picture', {}, data).then((resp) => ({
name: image.name,
size: image.size,
type: image.type,
url: `${BASE_URL + API_BASE}/picture/${resp.id}`,
}));
};
/**
@@ -197,14 +162,13 @@ export const uploadImage = (image: File): Promise<Image> => {
* @param emailAddress email for subscription
*/
export const emailVerificationForSubscribe = (emailAddress: string) =>
fetcher.post(stringifyUrl('/email/subscribe', { address: emailAddress }));
fetcher.post('/email/subscribe', { address: emailAddress });
/**
* Confirmation of email subscription to updates
* @param token confirmation token from email
*/
export const emailConfirmationForSubscribe = (token: string) =>
fetcher.post(stringifyUrl('/email/confirm', { tkn: token }));
export const emailConfirmationForSubscribe = (token: string) => fetcher.post('/email/confirm', { tkn: token });
/**
* Decline current subscription to updates
+22 -16
View File
@@ -1,3 +1,7 @@
jest.mock('./settings', () => ({
siteId: 'remark',
}));
import { RequestError } from 'utils/errorUtils';
import { API_BASE, BASE_URL } from './constants.config';
import fetcher, { JWT_HEADER, XSRF_HEADER } from './fetcher';
@@ -28,6 +32,8 @@ function mockFetch({ headers = {}, data = {}, ...props }: FetchImplementaitonPro
describe('fetcher', () => {
const headers = { [XSRF_HEADER]: '' };
const authUrl = `${BASE_URL}/auth?site=remark`;
const commentsUrl = `${BASE_URL}${API_BASE}/comments?site=remark`;
describe('methods', () => {
it('should send GET request', async () => {
@@ -36,7 +42,7 @@ describe('fetcher', () => {
mockFetch();
await fetcher.get('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'get', headers });
expect(window.fetch).toHaveBeenCalledWith(authUrl, { method: 'get', headers });
});
it('should send POST request', async () => {
expect.assertions(1);
@@ -44,7 +50,7 @@ describe('fetcher', () => {
mockFetch();
await fetcher.post('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'post', headers });
expect(window.fetch).toHaveBeenCalledWith(authUrl, { method: 'post', headers });
});
it('should send PUT request', async () => {
expect.assertions(1);
@@ -52,7 +58,7 @@ describe('fetcher', () => {
mockFetch();
await fetcher.put('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'put', headers });
expect(window.fetch).toHaveBeenCalledWith(authUrl, { method: 'put', headers });
});
it('should send DELETE request', async () => {
expect.assertions(1);
@@ -60,7 +66,7 @@ describe('fetcher', () => {
mockFetch();
await fetcher.delete('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth`, { method: 'delete', headers });
expect(window.fetch).toHaveBeenCalledWith(authUrl, { method: 'delete', headers });
});
});
@@ -69,9 +75,9 @@ describe('fetcher', () => {
expect.assertions(1);
mockFetch();
await fetcher.post('/auth/google/login');
await fetcher.post('/auth');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}/auth/google/login`, { method: 'post', headers });
expect(window.fetch).toHaveBeenCalledWith(authUrl, { method: 'post', headers });
});
it('should add API_BASE for requests to auth endpoints', async () => {
@@ -80,7 +86,7 @@ describe('fetcher', () => {
mockFetch();
await fetcher.post('/comments');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comments`, { method: 'post', headers });
expect(window.fetch).toHaveBeenCalledWith(commentsUrl, { method: 'post', headers });
});
});
@@ -106,16 +112,16 @@ describe('fetcher', () => {
expect(e.message).toBe('Not authorized.');
});
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comments`, {
expect(window.fetch).toHaveBeenCalledWith(commentsUrl, {
method: 'get',
headers: headersWithJwt,
});
// Check if `activeJwtToken` was cleaned
mockFetch({ headers });
await fetcher.get('/comments', { headers });
await fetcher.get('/comments');
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comments`, { method: 'get', headers });
expect(window.fetch).toHaveBeenCalledWith(commentsUrl, { method: 'get', headers });
});
});
@@ -123,16 +129,16 @@ describe('fetcher', () => {
it('should send JSON', async () => {
expect.assertions(1);
const json = { text: 'text' };
const data = { text: 'text' };
const headersWithContentType = { ...headers, 'Content-Type': 'application/json' };
mockFetch();
await fetcher.post('/comment', { json });
await fetcher.post('/comments', {}, data);
expect(window.fetch).toBeCalledWith(`${BASE_URL}${API_BASE}/comment`, {
expect(window.fetch).toBeCalledWith(commentsUrl, {
method: 'post',
headers: headersWithContentType,
body: JSON.stringify(json),
body: JSON.stringify(data),
});
});
it('should send form data', async () => {
@@ -142,9 +148,9 @@ describe('fetcher', () => {
const body = new FormData();
mockFetch();
await fetcher.post('/comment', { body, headers: headersWithMultipartData });
await fetcher.post('/comments', {}, body);
expect(window.fetch).toHaveBeenCalledWith(`${BASE_URL}${API_BASE}/comment`, {
expect(window.fetch).toHaveBeenCalledWith(commentsUrl, {
method: 'post',
body,
headers: headersWithMultipartData,
+39 -16
View File
@@ -3,6 +3,7 @@ import { httpErrorMap, httpMessages, RequestError } from 'utils/errorUtils';
import { BASE_URL, API_BASE } from './constants';
import { StaticStore } from './static-store';
import { getCookie } from './cookies';
import { siteId } from 'common/settings';
/** List of fetchers supported methods */
const METHODS = ['get', 'post', 'put', 'delete'] as const;
@@ -13,24 +14,41 @@ export const XSRF_HEADER = 'X-XSRF-TOKEN';
/** Cookie field with XSRF token */
export const XSRF_COOKIE = 'XSRF-TOKEN';
type Methods = typeof METHODS;
type FetchInit = Omit<RequestInit, 'headers'> & {
headers?: Record<string, string>;
json?: unknown;
query?: Record<string, string | number | undefined>;
};
type FetcherObject = Record<Methods[number], <T>(url: string, params?: FetchInit) => Promise<T>>;
type QueryParams = Record<string, string | number | undefined>;
type Payload = BodyInit | Record<string, unknown> | null;
type FetcherMethods = Record<'get' | 'delete', <T>(url: string, query?: QueryParams) => Promise<T>> &
Record<'put' | 'post', <T>(url: string, query?: QueryParams, body?: Payload) => Promise<T>>;
export function stringifyUrl(uri: string, query: QueryParams) {
const queryEntries = Object.entries(query);
const siteIdParam = `site=${encodeURIComponent(siteId)}`;
const baseUrl = uri.indexOf('/auth') === 0 ? BASE_URL : `${BASE_URL}${API_BASE}`;
const url = `${baseUrl}${uri}`;
if (queryEntries.length === 0) {
return `${url}?${siteIdParam}`;
}
const queryString = Object.entries(query)
.reduce(
(accum, [k, v]) => (v === undefined ? accum : [...accum, `${encodeURIComponent(k)}=${encodeURIComponent(v)}`]),
[siteIdParam] as string[]
)
.join('&');
return `${url}?${queryString}`;
}
/** JWT token received from server and will be send by each request, if it present */
let activeJwtToken: string | undefined;
const fetcher = METHODS.reduce((acc, method) => {
acc[method] = async (uri: string, params: FetchInit = {}) => {
const { headers = {}, json, ...fetchParams } = params;
const fetcher = METHODS.reduce<FetcherMethods>((acc, method) => {
acc[method] = async (uri: string, query: QueryParams = {}, body?: Payload) => {
// add api base if it's not auth request
// we use `indexOf` instead of `startsWidth` because we don't want to have another one polyfill for no reason
const baseUrl = uri.indexOf('/auth') === 0 ? BASE_URL : `${BASE_URL}${API_BASE}`;
const url = `${baseUrl}${uri}`;
const url = stringifyUrl(uri, query);
const headers: Record<string, string> = {};
const params: RequestInit = { method };
// Save token in memory and pass it into headers in case if storing cookies is disabled
if (activeJwtToken) {
@@ -38,13 +56,18 @@ const fetcher = METHODS.reduce((acc, method) => {
}
headers[XSRF_HEADER] = getCookie(XSRF_COOKIE) || '';
if (json) {
if (body instanceof FormData) {
headers['Content-Type'] = 'multipart/form-data';
params.body = body;
} else if (typeof body === 'object' && body !== null) {
headers['Content-Type'] = 'application/json';
fetchParams.body = JSON.stringify(json);
params.body = JSON.stringify(body);
} else {
params.body = body;
}
try {
const res = await fetch(url, { ...fetchParams, method, headers });
const res = await fetch(url, { ...params, headers });
// TODO: it should be clarified when frontend gets this header and what could be in it to simplify this logic and cover by tests
const date = (res.headers.has('date') && res.headers.get('date')) || '';
const timestamp = isNaN(Date.parse(date)) ? 0 : Date.parse(date);
@@ -93,6 +116,6 @@ const fetcher = METHODS.reduce((acc, method) => {
}
};
return acc;
}, {} as FetcherObject);
}, {} as FetcherMethods);
export default fetcher;