Browse Source

feat(gui-v2): use separate api instances with `useApi`

pull/2837/head
braks 2 years ago
parent
commit
48373f1ff3
  1. 2
      packages/nc-gui-v2/app.vue
  2. 40
      packages/nc-gui-v2/composables/useApi/index.ts
  3. 78
      packages/nc-gui-v2/composables/useApi/interceptors.ts
  4. 2
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  5. 89
      packages/nc-gui-v2/plugins/api.ts

2
packages/nc-gui-v2/app.vue

@ -8,7 +8,7 @@ import { navigateTo } from '#app'
const { $state } = useNuxtApp()
const { isLoading } = useApi()
const { isLoading } = useApi({ useGlobalInstance: true })
const sidebar = ref<HTMLDivElement>()

40
packages/nc-gui-v2/composables/useApi.ts → packages/nc-gui-v2/composables/useApi/index.ts

@ -1,8 +1,10 @@
import type { AxiosError, AxiosResponse } from 'axios'
import type { Api } from 'nocodb-sdk'
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import { Api } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { EventHook } from '@vueuse/core'
import { createEventHook, ref, useNuxtApp } from '#imports'
import type { EventHook, MaybeRef } from '@vueuse/core'
import { addAxiosInterceptors } from './interceptors'
import { createEventHook, ref, unref, useNuxtApp } from '#imports'
import type { NuxtApp } from '#app'
interface UseApiReturn<D = any, R = any> {
api: Api<any>
@ -13,10 +15,23 @@ interface UseApiReturn<D = any, R = any> {
onResponse: EventHook<AxiosResponse<D, R>>['on']
}
export function createApiInstance(app: NuxtApp, baseURL = 'http://localhost:8080') {
const api = new Api({
baseURL,
})
addAxiosInterceptors(api, app)
return api
}
/** todo: add props? */
type UseApiProps = never
interface UseApiProps<D = any> {
axiosConfig?: MaybeRef<AxiosRequestConfig<D>>
useGlobalInstance?: MaybeRef<boolean>
}
export function useApi<Data = any, RequestConfig = any>(_?: UseApiProps): UseApiReturn<Data, RequestConfig> {
export function useApi<Data = any, RequestConfig = any>(props: UseApiProps<Data> = {}): UseApiReturn<Data, RequestConfig> {
const isLoading = ref(false)
const error = ref(null)
@ -27,15 +42,18 @@ export function useApi<Data = any, RequestConfig = any>(_?: UseApiProps): UseApi
const responseHook = createEventHook<AxiosResponse<Data, RequestConfig>>()
const { $api } = useNuxtApp()
const api = unref(props.useGlobalInstance) ? useNuxtApp().$api : createApiInstance(useNuxtApp())
$api.instance.interceptors.request.use(
api.instance.interceptors.request.use(
(config) => {
error.value = null
response.value = null
isLoading.value = true
return config
return {
...config,
...unref(props),
}
},
(requestError) => {
errorHook.trigger(requestError)
@ -47,7 +65,7 @@ export function useApi<Data = any, RequestConfig = any>(_?: UseApiProps): UseApi
},
)
$api.instance.interceptors.response.use(
api.instance.interceptors.response.use(
(apiResponse) => {
responseHook.trigger(apiResponse as AxiosResponse<Data, RequestConfig>)
// can't properly typecast
@ -65,5 +83,5 @@ export function useApi<Data = any, RequestConfig = any>(_?: UseApiProps): UseApi
},
)
return { api: $api, isLoading, response, error, onError: errorHook.on, onResponse: responseHook.on }
return { api, isLoading, response, error, onError: errorHook.on, onResponse: responseHook.on }
}

78
packages/nc-gui-v2/composables/useApi/interceptors.ts

@ -0,0 +1,78 @@
import type { Api } from 'nocodb-sdk'
import { navigateTo, useRoute, useRouter } from '#imports'
import type { NuxtApp } from '#app'
const DbNotFoundMsg = 'Database config not found'
export function addAxiosInterceptors(api: Api<any>, app: NuxtApp) {
const router = useRouter()
const route = useRoute()
api.instance.interceptors.request.use((config) => {
config.headers['xc-gui'] = 'true'
if (app.$state.token.value) config.headers['xc-auth'] = app.$state.token.value
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
// config.headers['xc-preview'] = store.state.users.previewAs
}
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
if (route && route.params && route.params.shared_base_id) config.headers['xc-shared-base-id'] = route.params.shared_base_id
}
return config
})
// Return a successful response back to the calling service
api.instance.interceptors.response.use(
(response) => response,
// Handle Error
(error) => {
if (error.response && error.response.data && error.response.data.msg === DbNotFoundMsg) return router.replace('/project/0')
// Return any error which is not due to authentication back to the calling service
if (!error.response || error.response.status !== 401) {
return Promise.reject(error)
}
// Logout user if token refresh didn't work or user is disabled
if (error.config.url === '/auth/refresh-token') {
app.$state.signOut()
return Promise.reject(error)
}
// Try request again with new token
return api.instance
.post('/auth/refresh-token', null, {
withCredentials: true,
})
.then((token) => {
// New request with new token
const config = error.config
config.headers['xc-auth'] = token.data.token
app.$state.signIn(token.data.token)
return new Promise((resolve, reject) => {
api.instance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
})
.catch(async (error) => {
app.$state.signOut()
// todo: handle new user
navigateTo('/signIn')
return Promise.reject(error)
})
},
)
}

2
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue

@ -142,7 +142,7 @@ function openQuickImportDialog(type: string) {
<style scoped>
.nc-container {
height: calc(100vh + var(--header-height));
height: calc(100vh - var(--header-height) - 8px);
@apply overflow-hidden;
}

89
packages/nc-gui-v2/plugins/api.ts

@ -1,88 +1,7 @@
import { Api } from 'nocodb-sdk'
import { defineNuxtPlugin, navigateTo } from '#app'
import type { GlobalState } from '~/lib/types'
import { defineNuxtPlugin } from '#imports'
import { createApiInstance } from '~/composables/useApi'
export default defineNuxtPlugin((nuxtApp) => {
const api = new Api({
baseURL: 'http://localhost:8080',
})
addAxiosInterceptors(api, nuxtApp as any)
nuxtApp.provide('api', api)
/** injects a global api instance */
nuxtApp.provide('api', createApiInstance(nuxtApp))
})
const DbNotFoundMsg = 'Database config not found'
function addAxiosInterceptors(api: Api<any>, app: { $state: GlobalState }) {
const router = useRouter()
const route = useRoute()
api.instance.interceptors.request.use((config) => {
config.headers['xc-gui'] = 'true'
if (app.$state?.token.value) config.headers['xc-auth'] = app.$state.token.value
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
// config.headers['xc-preview'] = store.state.users.previewAs
}
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
if (route && route.params && route.params.shared_base_id) config.headers['xc-shared-base-id'] = route.params.shared_base_id
}
return config
})
// Return a successful response back to the calling service
api.instance.interceptors.response.use(
(response) => response,
// Handle Error
(error) => {
if (error.response && error.response.data && error.response.data.msg === DbNotFoundMsg) return router.replace('/project/0')
// Return any error which is not due to authentication back to the calling service
if (!error.response || error.response.status !== 401) {
return Promise.reject(error)
}
// Logout user if token refresh didn't work or user is disabled
if (error.config.url === '/auth/refresh-token') {
app.$state.signOut()
return Promise.reject(error)
}
// Try request again with new token
return api.instance
.post('/auth/refresh-token', null, {
withCredentials: true,
})
.then((token) => {
// New request with new token
const config = error.config
config.headers['xc-auth'] = token.data.token
app.$state.signIn(token.data.token)
return new Promise((resolve, reject) => {
api.instance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
})
.catch(async (error) => {
app.$state.signOut()
// todo: handle new user
navigateTo('/signIn')
return Promise.reject(error)
})
},
)
}

Loading…
Cancel
Save