From 30b07f3d34516c16c20b77c2812f4057ad2240c3 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Tue, 17 May 2022 13:44:49 +0300 Subject: [PATCH] Merge pull request #7871 from overleaf/ii-fetch-json-ts Convert fetch-json to TS GitOrigin-RevId: 78b0835aca4ba82c38004f37bb5c38ec7fb1b32c --- .../settings/context/user-email-context.tsx | 2 +- .../{fetch-json.js => fetch-json.ts} | 152 +++++++----------- services/web/types/window.ts | 1 + 3 files changed, 58 insertions(+), 97 deletions(-) rename services/web/frontend/js/infrastructure/{fetch-json.js => fetch-json.ts} (58%) diff --git a/services/web/frontend/js/features/settings/context/user-email-context.tsx b/services/web/frontend/js/features/settings/context/user-email-context.tsx index c8d017a6b6..9c87dc6385 100644 --- a/services/web/frontend/js/features/settings/context/user-email-context.tsx +++ b/services/web/frontend/js/features/settings/context/user-email-context.tsx @@ -206,7 +206,7 @@ function useUserEmails() { const { data, isLoading, isError, isSuccess, runAsync } = useAsync() const getEmails = useCallback(() => { - runAsync(getJSON('/user/emails?ensureAffiliation=true')) + runAsync(getJSON('/user/emails?ensureAffiliation=true')) .then(data => { dispatch(ActionCreators.setData(data)) }) diff --git a/services/web/frontend/js/infrastructure/fetch-json.js b/services/web/frontend/js/infrastructure/fetch-json.ts similarity index 58% rename from services/web/frontend/js/infrastructure/fetch-json.js rename to services/web/frontend/js/infrastructure/fetch-json.ts index 2fc8a380c4..689f1bb50f 100644 --- a/services/web/frontend/js/infrastructure/fetch-json.js +++ b/services/web/frontend/js/infrastructure/fetch-json.ts @@ -5,126 +5,86 @@ // - parse JSON response body, unless response is empty import OError from '@overleaf/o-error' -/** - * @typedef {Object} FetchOptions - * @extends RequestInit - * @property {Object} [body] - * @property {Boolean} [swallowAbortError] Set to false for throwing AbortErrors. - * @property {AbortSignal} [signal] Allows aborting a request via AbortController - */ +type FetchPath = string +// Custom config types are merged with `fetch`s RequestInit type +type FetchConfig = { + swallowAbortError?: boolean + body?: Record +} & Omit -/** - * @param {string} path - * @param {FetchOptions} [options] - */ -export function getJSON(path, options) { - return fetchJSON(path, { ...options, method: 'GET' }) +export function getJSON(path: FetchPath, options?: FetchConfig) { + return fetchJSON(path, { ...options, method: 'GET' }) } -/** - * @param {string} path - * @param {FetchOptions} [options] - */ -export function postJSON(path, options) { - return fetchJSON(path, { ...options, method: 'POST' }) +export function postJSON(path: FetchPath, options?: FetchConfig) { + return fetchJSON(path, { ...options, method: 'POST' }) } -/** - * @param {string} path - * @param {FetchOptions} [options] - */ -export function putJSON(path, options) { - return fetchJSON(path, { ...options, method: 'PUT' }) +export function putJSON(path: FetchPath, options?: FetchConfig) { + return fetchJSON(path, { ...options, method: 'PUT' }) } -/** - * @param {string} path - * @param {FetchOptions} [options] - */ -export function deleteJSON(path, options) { - return fetchJSON(path, { ...options, method: 'DELETE' }) +export function deleteJSON(path: FetchPath, options?: FetchConfig) { + return fetchJSON(path, { ...options, method: 'DELETE' }) } -/** - * @param {number} statusCode - * @returns {string} - */ -function getErrorMessageForStatusCode(statusCode) { - switch (statusCode) { - case 400: - return 'Bad Request' - case 401: - return 'Unauthorized' - case 403: - return 'Forbidden' - case 404: - return 'Not Found' - case 429: - return 'Too Many Requests' - case 500: - return 'Internal Server Error' - case 502: - return 'Bad Gateway' - case 503: - return 'Service Unavailable' - default: - return `Unexpected Error: ${statusCode}` +function getErrorMessageForStatusCode(statusCode?: number) { + if (!statusCode) { + return 'Unknown Error' } + + const statusCodes: { readonly [K: number]: string } = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 429: 'Too Many Requests', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + } + + return statusCodes[statusCode] ?? `Unexpected Error: ${statusCode}` } export class FetchError extends OError { - /** - * @param {string} message - * @param {string} url - * @param {FetchOptions} [options] - * @param {Response} [response] - * @param {Object} [data] - */ - constructor(message, url, options, response, data) { + constructor( + message: string, + public url: string, + public options?: RequestInit, + public response?: Response, + public data?: any + ) { // On HTTP2, the `statusText` property is not set, // so this `message` will be undefined. We need to // set a message based on the response `status`, so // our error UI rendering will work if (!message) { - message = getErrorMessageForStatusCode(response.status) + message = getErrorMessageForStatusCode(response?.status) } super(message, { statusCode: response ? response.status : undefined }) - this.url = url - this.options = options - this.response = response - this.data = data } - /** - * @returns {string} - */ getUserFacingMessage() { const statusCode = this.response?.status const defaultMessage = getErrorMessageForStatusCode(statusCode) const message = this.data?.message?.text || this.data?.message if (message && message !== defaultMessage) return message - switch (statusCode) { - case 400: - return 'Invalid Request. Please correct the data and try again.' - case 403: - return 'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.' - case 429: - return 'Too many attempts. Please wait for a while and try again.' - default: - return 'Something went wrong. Please try again.' + const statusCodes: { readonly [K: number]: string } = { + 400: 'Invalid Request. Please correct the data and try again.', + 403: 'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.', + 429: 'Too many attempts. Please wait for a while and try again.', } + + return statusCode && statusCodes[statusCode] + ? statusCodes[statusCode] + : 'Something went wrong. Please try again.' } } -/** - * @param {string} path - * @param {FetchOptions} [options] - * - * @return Promise - */ -function fetchJSON( - path, +function fetchJSON( + path: FetchPath, { body = {}, headers = {}, @@ -132,9 +92,9 @@ function fetchJSON( credentials = 'same-origin', swallowAbortError = true, ...otherOptions - } + }: FetchConfig ) { - const options = { + const options: RequestInit = { ...otherOptions, headers: { ...headers, @@ -155,7 +115,7 @@ function fetchJSON( // after a component has unmounted. // `resolve` will be called when the request succeeds, `reject` will be called when the request fails, // but nothing will be called if the request is cancelled via an AbortController. - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { fetch(path, options).then( response => { return parseResponseBody(response).then( @@ -206,13 +166,13 @@ function fetchJSON( }) } -/** - * @param {Response} response - * @returns {Promise} - */ -async function parseResponseBody(response) { +async function parseResponseBody(response: Response) { const contentType = response.headers.get('Content-Type') + if (!contentType) { + return {} + } + if (/application\/json/.test(contentType)) { return response.json() } diff --git a/services/web/types/window.ts b/services/web/types/window.ts index 55e6eacba4..54c0427ac8 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -4,6 +4,7 @@ import { OAuthProviders } from './oauth-providers' declare global { // eslint-disable-next-line no-unused-vars interface Window { + csrfToken: string sl_debugging: boolean user: { id: string