fix(regression): postpone the integration of openapi-client and instead manually type all requests #2398
This commit is contained in:
parent
910a6ab157
commit
43c1bee971
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BackendError, FileSystem, Library, PrivacyLevel } from '~/types'
|
import type { BackendError, FileSystem, Library, PrivacyLevel } from '~/types'
|
||||||
import type { paths } from '~/generated/types'
|
|
||||||
import type { VueUploadItem } from 'vue-upload-component'
|
import type { VueUploadItem } from 'vue-upload-component'
|
||||||
|
import type { paths } from '~/generated/types'
|
||||||
|
|
||||||
import { computed, ref, reactive, watch, nextTick } from 'vue'
|
import { computed, ref, reactive, watch, nextTick } from 'vue'
|
||||||
import { useEventListener, useIntervalFn } from '@vueuse/core'
|
import { useEventListener, useIntervalFn } from '@vueuse/core'
|
||||||
|
@ -9,7 +9,6 @@ import { humanSize, truncate } from '~/utils/filters'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { sortBy } from 'lodash-es'
|
import { sortBy } from 'lodash-es'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { useClient } from '~/ui/composables/useClient'
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
@ -42,7 +41,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const { get, post } = useClient('libraries')
|
|
||||||
|
|
||||||
const upload = ref()
|
const upload = ref()
|
||||||
const currentTab = ref('uploads')
|
const currentTab = ref('uploads')
|
||||||
|
@ -86,31 +84,31 @@ const library = ref<Library>()
|
||||||
|
|
||||||
// New implementation with `useClient`:
|
// New implementation with `useClient`:
|
||||||
|
|
||||||
watch(privacyLevel, (newValue) =>
|
// watch(privacyLevel, (newValue) =>
|
||||||
get({
|
// get({
|
||||||
privacy_level: newValue,
|
// privacy_level: newValue,
|
||||||
scope: 'me'
|
// scope: 'me'
|
||||||
})
|
// })
|
||||||
.then((data) =>
|
// .then((data) =>
|
||||||
library.value = data?.results.find(({name}) => name === privacyLevel.value)
|
// library.value = data?.results.find(({name}) => name === privacyLevel.value)
|
||||||
),
|
// ),
|
||||||
{ immediate: true }
|
// { immediate: true }
|
||||||
)
|
// )
|
||||||
|
|
||||||
// Old implementation:
|
// Old implementation:
|
||||||
|
|
||||||
// watch(privacyLevel, async(newValue) => { try {
|
watch(privacyLevel, async(newValue) => { try {
|
||||||
// const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
|
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
|
||||||
// params: {
|
params: {
|
||||||
// privacy_level: privacyLevel.value,
|
privacy_level: privacyLevel.value,
|
||||||
// scope: 'me'
|
scope: 'me'
|
||||||
// }
|
}
|
||||||
// })
|
})
|
||||||
|
|
||||||
// library.value = response.data.results.find(({name})=>name===privacyLevel.value)
|
library.value = response.data.results.find(({name})=>name===privacyLevel.value)
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// useErrorHandler(error as Error)
|
useErrorHandler(error as Error)
|
||||||
// }}, { immediate: true })
|
}}, { immediate: true })
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,47 +1,139 @@
|
||||||
import type { paths } from '~/generated/types.ts'
|
import type { paths } from '~/generated/types.ts'
|
||||||
import type { Simplify } from 'type-fest'
|
import type { APIErrorResponse, BackendError, RateLimitStatus } from '~/types'
|
||||||
|
|
||||||
import axios from 'axios'
|
import createClient from 'openapi-fetch'
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
|
||||||
|
import { parseAPIErrors } from '~/utils'
|
||||||
|
import { i18n } from '~/init/locale'
|
||||||
|
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
// Note [WIP] that this module is Work in Progress!
|
||||||
|
// TODO: Replace all `axios` calls with this client
|
||||||
|
|
||||||
|
const { t } = i18n.global
|
||||||
|
const logger = useLogger()
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const prefix = "/api/v2/" as const;
|
const prefix = "/api/v2/" as const;
|
||||||
type Prefix = typeof prefix;
|
|
||||||
|
|
||||||
type RemovePrefixAndSuffix<T extends string> =
|
const client = createClient<paths>({ baseUrl: `${store.state.instance.instanceUrl}${prefix}` });
|
||||||
T extends `${Prefix}${infer Infix}/` ? Infix : never
|
client.use({
|
||||||
|
/*
|
||||||
type Path = RemovePrefixAndSuffix<Simplify<keyof paths>>
|
TODO: Check if we need these:
|
||||||
|
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||||
type Get<TPath extends Path> = paths[`${Prefix}${TPath}/`]['get']
|
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
|
||||||
type Post<TPath extends Path> = paths[`${Prefix}${TPath}/`]['post']
|
|
||||||
|
|
||||||
type GetRequestParameters<TPath extends Path> =
|
|
||||||
Get<TPath> extends { parameters: { query? : any } }
|
|
||||||
? Get<TPath>['parameters']['query']
|
|
||||||
: Get<TPath> extends { parameters: any }
|
|
||||||
? Get<TPath>['parameters']
|
|
||||||
: never
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 200
|
|
||||||
*/
|
*/
|
||||||
type OK<TPath extends Path> =
|
|
||||||
Get<TPath> extends { responses: { ['200'] : { content: { ['application/json']: any } } } }
|
|
||||||
? Get<TPath>['responses']['200']['content']['application/json']
|
|
||||||
: never
|
|
||||||
|
|
||||||
type PostRequestJson<TPath extends Path> =
|
async onRequest({ request, options }) {
|
||||||
Post<TPath> extends { requestBody: { content: { ['application/json']: any } } }
|
if (store.state.auth.oauth.accessToken) {
|
||||||
? Post<TPath>['requestBody']['content']['application/json']
|
request.headers.set("Authorization", store.getters['auth/header'])
|
||||||
: never
|
}
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
async onResponse({ request, response, options }) {
|
||||||
* 201
|
return response
|
||||||
*/
|
},
|
||||||
type Created<TPath extends Path> =
|
|
||||||
Post<TPath> extends { responses: { ['201']: { content: { ['application/json']: any } } } }
|
async onError({ error: unknownError }) {
|
||||||
? Post<TPath>['responses']['201']['content']['application/json']
|
const error = unknownError as BackendError
|
||||||
: never
|
error.backendErrors = []
|
||||||
|
error.isHandled = false
|
||||||
|
|
||||||
|
if (store.state.auth.authenticated && !store.state.auth.oauth.accessToken && error.response?.status === 401) {
|
||||||
|
store.commit('auth/authenticated', false)
|
||||||
|
logger.warn('Received 401 response from API, redirecting to login form', router.currentRoute.value.fullPath)
|
||||||
|
await router.push({ name: 'login', query: { next: router.currentRoute.value.fullPath } })
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (error.response?.status) {
|
||||||
|
case 404:
|
||||||
|
if (error.response?.data === 'Radio doesn\'t have more candidates') {
|
||||||
|
error.backendErrors.push(error.response.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
error.backendErrors.push('Resource not found')
|
||||||
|
error.isHandled = true
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
// @ts-expect-error TS does not know about .data structure
|
||||||
|
content: error.response?.data?.detail ?? error.response?.data ?? 'Resource not found',
|
||||||
|
class: 'error'
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
case 403:
|
||||||
|
error.backendErrors.push('Permission denied')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 429: {
|
||||||
|
let message
|
||||||
|
const rateLimitStatus: RateLimitStatus = {
|
||||||
|
limit: error.response?.headers['x-ratelimit-limit'],
|
||||||
|
description: error.response?.headers['x-ratelimit-scope'],
|
||||||
|
remaining: error.response?.headers['x-ratelimit-remaining'],
|
||||||
|
duration: error.response?.headers['x-ratelimit-duration'],
|
||||||
|
available_seconds: parseInt(error.response?.headers['retry-after'] ?? '60'),
|
||||||
|
reset: error.response?.headers['x-ratelimit-reset'],
|
||||||
|
reset_seconds: error.response?.headers['x-ratelimit-resetseconds'],
|
||||||
|
/* The following threewere not defined. TODO: research correct values */
|
||||||
|
id: '',
|
||||||
|
available: 100,
|
||||||
|
rate: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateLimitStatus.available_seconds) {
|
||||||
|
const tryAgain = moment().add(rateLimitStatus.available_seconds, 's').toNow(true)
|
||||||
|
message = t('init.axios.rateLimitDelay', { delay: tryAgain })
|
||||||
|
} else {
|
||||||
|
message = t('init.axios.rateLimitLater')
|
||||||
|
}
|
||||||
|
|
||||||
|
error.backendErrors.push(message)
|
||||||
|
error.isHandled = true
|
||||||
|
store.commit('ui/addMessage', {
|
||||||
|
content: message,
|
||||||
|
date: new Date(),
|
||||||
|
class: 'error'
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.error('This client is rate-limited!', rateLimitStatus)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 500:
|
||||||
|
error.backendErrors.push('A server error occurred')
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (error.response?.data as object) {
|
||||||
|
const data = error.response?.data as Record<string, unknown>
|
||||||
|
if (data?.detail) {
|
||||||
|
error.backendErrors.push(data.detail as string)
|
||||||
|
} else {
|
||||||
|
error.rawPayload = data as APIErrorResponse
|
||||||
|
const parsedErrors = parseAPIErrors(data as APIErrorResponse)
|
||||||
|
error.backendErrors = [...error.backendErrors, ...parsedErrors]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.backendErrors.length === 0) {
|
||||||
|
error.backendErrors.push('An unknown error occurred, ensure your are connected to the internet and your funkwhale instance is up and running')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do something with response error
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
|
||||||
|
/* TODO: Check if we need to handle refreshAuth = async (failedRequest: AxiosError) */
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -54,23 +146,19 @@ const result1 = get({ query: { q: 'test' } })
|
||||||
```
|
```
|
||||||
*
|
*
|
||||||
* @param path The path to create a client for. Check the `paths` type in '~/generated/types.ts' to find all available paths.
|
* @param path The path to create a client for. Check the `paths` type in '~/generated/types.ts' to find all available paths.
|
||||||
|
* @param variable
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export const useClient = <TPath extends Path>( path: TPath ) => ({
|
export const useClient = ({
|
||||||
|
|
||||||
get: async (parameters: GetRequestParameters<TPath>) =>
|
get: client.GET,
|
||||||
await axios.get<OK<TPath>>(`${prefix}${path}/`, {
|
|
||||||
params: parameters
|
|
||||||
})
|
|
||||||
.then(({ data }) => { return data })
|
|
||||||
.catch (useErrorHandler),
|
|
||||||
|
|
||||||
post: async (requestBody: PostRequestJson<TPath> ) =>
|
post:client.POST,
|
||||||
await axios.post<Created<TPath>>(`${prefix}${path}/`, {
|
|
||||||
params: requestBody
|
|
||||||
})
|
|
||||||
.then(({ data }) => { return data })
|
|
||||||
.catch (useErrorHandler)
|
|
||||||
|
|
||||||
// TODO: Add put, patch, delete, head, options, trace
|
put: client.PUT,
|
||||||
|
|
||||||
|
patch: client.PATCH,
|
||||||
|
|
||||||
|
delete: client.DELETE,
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue