useApiGateway.ts 3.19 KB
import { createSharedComposable } from '@vueuse/core'
import type { AsyncDataOptions } from '#app'

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'

type ApiDataOptions<T> = {
  default?: () => T
  transform?: (input: unknown) => T
  lazy?: boolean
  server?: boolean
  immediate?: boolean
  key?: string
  watch?: AsyncDataOptions<T>['watch']
  auth?: boolean
  remotePath?: string
}

type ApiRequestOptions = {
  method?: HttpMethod
  body?: RequestInit['body'] | object | null
  headers?: Record<string, string>
  auth?: boolean
  remotePath?: string
}

function trimTrailingSlash(value: string) {
  return value.replace(/\/+$/, '')
}

function normalizePath(path: string) {
  if (path.startsWith('/')) {
    return path
  }

  return `/${path}`
}

function localPathToRemote(localPath: string) {
  if (localPath.startsWith('/api/')) {
    return localPath.slice(4)
  }

  return localPath
}

const _useApiGateway = () => {
  const runtimeConfig = useRuntimeConfig()
  const token = useState<string | null>('auth-token', () => null)

  const apiBase = computed(() => {
    const rawBase = typeof runtimeConfig.public.apiBase === 'string' ? runtimeConfig.public.apiBase.trim() : ''

    if (!rawBase) {
      return ''
    }

    return trimTrailingSlash(rawBase)
  })

  const useRemote = computed(() => apiBase.value.length > 0)

  const resolvePath = (localPath: string, remotePath?: string) => {
    if (!useRemote.value) {
      return normalizePath(localPath)
    }

    const targetPath = remotePath ?? localPathToRemote(localPath)
    return normalizePath(targetPath)
  }

  const buildHeaders = (auth: boolean, customHeaders?: Record<string, string>) => {
    const headers = {
      ...(customHeaders || {})
    }

    if (auth && token.value) {
      headers.Authorization = `Bearer ${token.value}`
    }

    if (Object.keys(headers).length === 0) {
      return undefined
    }

    return headers
  }

  const request = <T>(localPath: string, options: ApiRequestOptions = {}) => {
    const {
      method = 'GET',
      body,
      headers,
      auth = true,
      remotePath
    } = options

    return $fetch<T>(resolvePath(localPath, remotePath), {
      baseURL: useRemote.value ? apiBase.value : undefined,
      method,
      body,
      headers: buildHeaders(auth, headers)
    })
  }

  const useApiFetch = <T>(localPath: string, options: ApiDataOptions<T> = {}) => {
    const {
      default: defaultValue,
      transform,
      lazy,
      server,
      immediate,
      key,
      watch,
      auth = true,
      remotePath
    } = options

    const dataKey = key ?? `${useRemote.value ? 'remote' : 'local'}:${resolvePath(localPath, remotePath)}`

    return useAsyncData<T>(
      dataKey,
      async () => {
        const raw = await request<unknown>(localPath, {
          method: 'GET',
          auth,
          remotePath
        })

        if (transform) {
          return transform(raw)
        }

        return raw as T
      },
      {
        default: defaultValue,
        lazy,
        server,
        immediate,
        watch
      }
    )
  }

  return {
    apiBase,
    useRemote,
    resolvePath,
    useApiFetch,
    request
  }
}

export const useApiGateway = createSharedComposable(_useApiGateway)