Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch sidecar Auspice JSON if .root_sequence is not on the tree #1460

Merged
merged 7 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/nextclade-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
},
"dependencies": {
"@floating-ui/react": "0.26.1",
"@hapi/accept": "6.0.3",
"@hapi/content": "6.0.0",
"animate.css": "4.1.1",
"auspice": "2.53.0",
"autoprefixer": "10.4.5",
Expand Down Expand Up @@ -200,6 +202,7 @@
"@types/friendly-errors-webpack-plugin": "0.1.4",
"@types/fs-extra": "9.0.13",
"@types/glob": "7.2.0",
"@types/hapi__content": "6.0.3",
"@types/history": "4.7.11",
"@types/intercept-stdout": "0.1.0",
"@types/jest": "27.4.1",
Expand Down
53 changes: 44 additions & 9 deletions packages/nextclade-web/src/io/axiosFetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import { isNil } from 'lodash'
import { isNil, isString } from 'lodash'
import { mediaTypes as parseAcceptHeader } from '@hapi/accept'
import { ContentType, type as parseContentTypeHeader } from '@hapi/content'
import { ErrorFatal } from 'src/helpers/ErrorFatal'
import { sanitizeError } from 'src/helpers/sanitizeError'

export interface RequestConfig extends AxiosRequestConfig {
// Check that MIME type of response's Content-Type header is compatible with at least one of MIME types in the request's Accept header
strictAccept?: boolean
}

export class HttpRequestError extends Error {
public readonly url?: string
public readonly status?: number | string
Expand Down Expand Up @@ -36,10 +43,27 @@ export function validateUrl(url?: string): string {
return url
}

export async function axiosFetch<TData = unknown>(
url_: string | undefined,
options?: AxiosRequestConfig,
): Promise<TData> {
export interface CheckMimeTypesResult {
isCompatible: boolean
acceptTypes?: string[]
contentType?: ContentType
acceptHeader?: string | number | boolean
contentTypeHeader?: string
}

export function checkMimeTypes(req?: RequestConfig, res?: AxiosResponse): CheckMimeTypesResult {
const acceptHeader = req?.headers?.Accept
const contentTypeHeader = res?.headers['content-type']
if (isString(acceptHeader) && isString(contentTypeHeader)) {
const acceptTypes = parseAcceptHeader(acceptHeader)
const contentType = parseContentTypeHeader(contentTypeHeader)
const isCompatible = acceptTypes.includes(contentType.mime)
return { isCompatible, acceptTypes, contentType, acceptHeader, contentTypeHeader }
}
return { isCompatible: false, acceptHeader, contentTypeHeader }
}

export async function axiosFetch<TData = unknown>(url_: string | undefined, options?: RequestConfig): Promise<TData> {
const url = validateUrl(url_)

let res
Expand All @@ -53,6 +77,17 @@ export async function axiosFetch<TData = unknown>(
throw new Error(`Unable to fetch: request to URL "${url}" resulted in no data`)
}

if (options?.strictAccept) {
const mime = checkMimeTypes(options, res)
if (!mime.isCompatible) {
const accept = mime.acceptHeader ?? ''
const contentType = mime.contentTypeHeader ?? ''
throw new Error(
`Unable to fetch: request to URL "${url}" resulted in incompatible MIME type: Content-Type was "${contentType}", while Accept was "${accept}"`,
)
}
}

return res.data as TData
}

Expand All @@ -65,7 +100,7 @@ export async function axiosFetchMaybe(url?: string): Promise<string | undefined>

export async function axiosFetchOrUndefined<TData = unknown>(
url: string | undefined,
options?: AxiosRequestConfig,
options?: RequestConfig,
): Promise<TData | undefined> {
try {
return await axiosFetch<TData>(url, options)
Expand All @@ -77,7 +112,7 @@ export async function axiosFetchOrUndefined<TData = unknown>(
/**
* This version skips any transforms (such as JSON parsing) and returns plain string
*/
export async function axiosFetchRaw(url: string | undefined, options?: AxiosRequestConfig): Promise<string> {
export async function axiosFetchRaw(url: string | undefined, options?: RequestConfig): Promise<string> {
return axiosFetch(url, { ...options, transformResponse: [] })
}

Expand All @@ -88,7 +123,7 @@ export async function axiosFetchRawMaybe(url?: string): Promise<string | undefin
return axiosFetchRaw(url)
}

export async function axiosHead(url: string | undefined, options?: AxiosRequestConfig): Promise<AxiosResponse> {
export async function axiosHead(url: string | undefined, options?: RequestConfig): Promise<AxiosResponse> {
if (isNil(url)) {
throw new ErrorFatal(`Attempted to fetch from an invalid URL: '${url}'`)
}
Expand All @@ -102,7 +137,7 @@ export async function axiosHead(url: string | undefined, options?: AxiosRequestC

export async function axiosHeadOrUndefined(
url: string | undefined,
options?: AxiosRequestConfig,
options?: RequestConfig,
): Promise<AxiosResponse | undefined> {
try {
return await axiosHead(url, options)
Expand Down
18 changes: 16 additions & 2 deletions packages/nextclade-web/src/io/fetchSingleDatasetAuspice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { isEmpty } from 'lodash'
import { attrStrMaybe, AuspiceTree, Dataset } from 'src/types'
import { removeTrailingSlash } from 'src/io/url'
import { axiosFetch } from 'src/io/axiosFetch'
import { axiosFetch, axiosFetchOrUndefined } from 'src/io/axiosFetch'

export async function fetchSingleDatasetAuspice(datasetJsonUrl_: string) {
const datasetJsonUrl = removeTrailingSlash(datasetJsonUrl_)

const auspiceJson = await axiosFetch<AuspiceTree>(datasetJsonUrl, {
headers: { Accept: 'application/json, text/plain, */*' },
headers: {
Accept: 'application/vnd.nextstrain.dataset.main+json;q=1, application/json;q=0.9, text/plain;q=0.8, */*;q=0.1',
},
})

if (isEmpty(auspiceJson.root_sequence)) {
const sidecar = await axiosFetchOrUndefined<Record<string, string>>(datasetJsonUrl, {
headers: { Accept: 'application/vnd.nextstrain.dataset.root-sequence+json' },
ivan-aksamentov marked this conversation as resolved.
Show resolved Hide resolved
strictAccept: true,
})
if (!isEmpty(sidecar)) {
auspiceJson.root_sequence = sidecar
}
}

const pathogen = auspiceJson.meta.extensions?.nextclade?.pathogen

const name =
Expand Down
4 changes: 3 additions & 1 deletion packages/nextclade-web/src/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

@import './components/Results';

html, body, #__next {
html,
body,
#__next {
overflow: hidden;
height: 100%;
width: 100%;
Expand Down
34 changes: 34 additions & 0 deletions packages/nextclade-web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,33 @@
protobufjs "^7.0.0"
yargs "^16.2.0"

"@hapi/[email protected]":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab"
integrity sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==
dependencies:
"@hapi/boom" "^10.0.1"
"@hapi/hoek" "^11.0.2"

"@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685"
integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==
dependencies:
"@hapi/hoek" "^11.0.2"

"@hapi/[email protected]":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@hapi/content/-/content-6.0.0.tgz#2427af3bac8a2f743512fce2a70cbdc365af29df"
integrity sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==
dependencies:
"@hapi/boom" "^10.0.0"

"@hapi/hoek@^11.0.2":
version "11.0.4"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.4.tgz#42a7f244fd3dd777792bfb74b8c6340ae9182f37"
integrity sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==

"@hot-loader/react-dom@^16.13.0":
version "16.14.0"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.14.0.tgz#3cfc64e40bb78fa623e59b582b8f09dcdaad648a"
Expand Down Expand Up @@ -3434,6 +3461,13 @@
dependencies:
"@types/node" "*"

"@types/[email protected]":
version "6.0.3"
resolved "https://registry.yarnpkg.com/@types/hapi__content/-/hapi__content-6.0.3.tgz#4e3820d8d07ae90de955263622e9c56ee177bd01"
integrity sha512-J0TOzZIy99b9G2ujGXpC7369wohXLCLwNfxEFN+X1w0oYOEhQ5+ViKweCZ919fGVp9U40QAjUc/LD1h1CVFCpQ==
dependencies:
"@types/node" "*"

"@types/hast@^2.0.0":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
Expand Down