fix(regression): postpone the integration of openapi-client and instead manually type all requests #2398

This commit is contained in:
upsiflu 2025-02-13 12:19:42 +01:00
parent 910a6ab157
commit 43c1bee971
2 changed files with 158 additions and 72 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { BackendError, FileSystem, Library, PrivacyLevel } from '~/types'
import type { paths } from '~/generated/types'
import type { VueUploadItem } from 'vue-upload-component'
import type { paths } from '~/generated/types'
import { computed, ref, reactive, watch, nextTick } from 'vue'
import { useEventListener, useIntervalFn } from '@vueuse/core'
@ -9,7 +9,6 @@ import { humanSize, truncate } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { sortBy } from 'lodash-es'
import { useStore } from '~/store'
import { useClient } from '~/ui/composables/useClient'
import axios from 'axios'
@ -42,7 +41,6 @@ const props = withDefaults(defineProps<Props>(), {
const { t } = useI18n()
const store = useStore()
const { get, post } = useClient('libraries')
const upload = ref()
const currentTab = ref('uploads')
@ -86,31 +84,31 @@ const library = ref<Library>()
// New implementation with `useClient`:
watch(privacyLevel, (newValue) =>
get({
privacy_level: newValue,
scope: 'me'
})
.then((data) =>
library.value = data?.results.find(({name}) => name === privacyLevel.value)
),
{ immediate: true }
)
// watch(privacyLevel, (newValue) =>
// get({
// privacy_level: newValue,
// scope: 'me'
// })
// .then((data) =>
// library.value = data?.results.find(({name}) => name === privacyLevel.value)
// ),
// { immediate: true }
// )
// Old implementation:
// watch(privacyLevel, async(newValue) => { try {
// const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
// params: {
// privacy_level: privacyLevel.value,
// scope: 'me'
// }
// })
watch(privacyLevel, async(newValue) => { try {
const response = await axios.get<paths['/api/v2/libraries/']['get']['responses']['200']['content']['application/json']>('libraries/', {
params: {
privacy_level: privacyLevel.value,
scope: 'me'
}
})
// library.value = response.data.results.find(({name})=>name===privacyLevel.value)
// } catch (error) {
// useErrorHandler(error as Error)
// }}, { immediate: true })
library.value = response.data.results.find(({name})=>name===privacyLevel.value)
} catch (error) {
useErrorHandler(error as Error)
}}, { immediate: true })
//

View File

@ -1,47 +1,139 @@
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 useErrorHandler from '~/composables/useErrorHandler'
import createClient from 'openapi-fetch'
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;
type Prefix = typeof prefix;
type RemovePrefixAndSuffix<T extends string> =
T extends `${Prefix}${infer Infix}/` ? Infix : never
const client = createClient<paths>({ baseUrl: `${store.state.instance.instanceUrl}${prefix}` });
client.use({
/*
TODO: Check if we need these:
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
*/
type Path = RemovePrefixAndSuffix<Simplify<keyof paths>>
async onRequest({ request, options }) {
if (store.state.auth.oauth.accessToken) {
request.headers.set("Authorization", store.getters['auth/header'])
}
return request;
},
type Get<TPath extends Path> = paths[`${Prefix}${TPath}/`]['get']
type Post<TPath extends Path> = paths[`${Prefix}${TPath}/`]['post']
async onResponse({ request, response, options }) {
return response
},
type GetRequestParameters<TPath extends Path> =
Get<TPath> extends { parameters: { query? : any } }
? Get<TPath>['parameters']['query']
: Get<TPath> extends { parameters: any }
? Get<TPath>['parameters']
: never
async onError({ error: unknownError }) {
const error = unknownError as BackendError
error.backendErrors = []
error.isHandled = false
/**
* 200
*/
type OK<TPath extends Path> =
Get<TPath> extends { responses: { ['200'] : { content: { ['application/json']: any } } } }
? Get<TPath>['responses']['200']['content']['application/json']
: never
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 } })
}
type PostRequestJson<TPath extends Path> =
Post<TPath> extends { requestBody: { content: { ['application/json']: any } } }
? Post<TPath>['requestBody']['content']['application/json']
: never
switch (error.response?.status) {
case 404:
if (error.response?.data === 'Radio doesn\'t have more candidates') {
error.backendErrors.push(error.response.data)
break
}
/**
* 201
*/
type Created<TPath extends Path> =
Post<TPath> extends { responses: { ['201']: { content: { ['application/json']: any } } } }
? Post<TPath>['responses']['201']['content']['application/json']
: never
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.
*/
export const useClient = <TPath extends Path>( path: TPath ) => ({
* @param variable
*
*/
export const useClient = ({
get: async (parameters: GetRequestParameters<TPath>) =>
await axios.get<OK<TPath>>(`${prefix}${path}/`, {
params: parameters
})
.then(({ data }) => { return data })
.catch (useErrorHandler),
get: client.GET,
post: async (requestBody: PostRequestJson<TPath> ) =>
await axios.post<Created<TPath>>(`${prefix}${path}/`, {
params: requestBody
})
.then(({ data }) => { return data })
.catch (useErrorHandler)
post:client.POST,
// TODO: Add put, patch, delete, head, options, trace
put: client.PUT,
patch: client.PATCH,
delete: client.DELETE,
})