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"> <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 })
// //

View File

@ -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,
}) })