Skip to content
Snippets Groups Projects
api.ts 4.53 KiB
Newer Older
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

type ID = number | string

class APIError extends Error {
  constructor(message: string) {
    super(message)
  }
}

class APIResponseError extends APIError {
  response: Response

  constructor(message: string, response: Response) {
    super(message)
    this.response = response
  }
}

type APIStoreOptions = {
  getRequestDefaults: () => RequestInit
}

function createURLBuilder(basepath: string, useTrailingSlash = true) {
  // Strip all trailing slashes from the basepath.
  // We handle slashes when building URLs.
  basepath = basepath.replace(/\/*$/, '')

  return function buildURL(...subPaths: string[] | [...string[], string | URLSearchParams]) {
    let params
    if (subPaths.at(-1) instanceof URLSearchParams) {
      params = subPaths.pop()
    }
    if (subPaths.some((path) => String(path).includes('/'))) {
      throw new Error('Subpaths must not contain slashes')
    }
    const subPath = subPaths.length > 0 ? '/' + subPaths.join('/') : ''
    const url = basepath + subPath + (useTrailingSlash ? '/' : '')
    return params ? url + `?${params}` : url
  }
}

export const createTankURL = createURLBuilder(import.meta.env.VUE_APP_API_TANK, false)
export const createSteeringURL = createURLBuilder(import.meta.env.VUE_APP_API_STEERING)

export function createAPIStore<T extends { id: ID }>(
  storeId: string,
  endpoint: string,
  options?: APIStoreOptions,
) {
  return defineStore(storeId, () => {
    const itemMap = ref<Map<ID, T>>(new Map())
    const items = computed<T[]>(() => Array.from(itemMap.value.values()))
    const currentItemId = ref<ID>()
    const currentItem = computed(() => items.value.find((item) => item.id === currentItemId.value))
    const error = ref<Error>()

    function maybeRaiseResponse(response: Response) {
      if (!response.ok) {
        const _error = new APIResponseError(
          `Failure response when executing when interacting with ${response.url}`,
          response,
        )
        error.value = _error
        throw _error
      }
    }

    function createRequest(
      url: string,
      customRequestData: RequestInit | undefined,
      defaultRequestData?: RequestInit | undefined,
    ) {
      return new Request(url, {
        ...(defaultRequestData ?? {}),
        ...(options?.getRequestDefaults?.() ?? {}),
        ...(customRequestData ?? {}),
      })
    }

    async function list(requestInit?: RequestInit): Promise<T[]> {
      const res = await fetch(createRequest(endpoint, requestInit))
      maybeRaiseResponse(res)
      const _items: T[] = await res.json()
      for (const item of _items) {
        itemMap.value.set(item.id, item)
      }
      return _items
    }

    async function retrieve(id: ID, requestInit?: RequestInit): Promise<T | null> {
      const res = await fetch(createRequest(`${endpoint}/${id}`, requestInit))
      if (res.status === 404) {
        itemMap.value.delete(id)
        return null
      } else {
        maybeRaiseResponse(res)
        const obj: T = await res.json()
        itemMap.value.set(obj.id, obj)
        return obj
      }
    }

    async function update(
      id: ID,
      data: Partial<T> | FormData,
      requestInit?: RequestInit,
    ): Promise<T> {
      const res = await fetch(
        createRequest(`${endpoint}/${id}`, requestInit, {
          method: 'PUT',
          headers: data instanceof FormData ? undefined : { 'Content-Type': 'application/json' },
          body: data instanceof FormData ? data : JSON.stringify(data),
        }),
      )
      maybeRaiseResponse(res)
      const obj: T = await res.json()
      itemMap.value.set(obj.id, obj)
      return obj
    }

    async function create(data: Partial<T> | FormData, requestInit?: RequestInit): Promise<T> {
      const res = await fetch(
        createRequest(endpoint, requestInit, {
          method: 'POST',
          headers: data instanceof FormData ? undefined : { 'Content-Type': 'application/json' },
          body: data instanceof FormData ? data : JSON.stringify(data),
        }),
      )
      maybeRaiseResponse(res)
      const obj = await res.json()
      itemMap.value.set(obj.id, obj)
      return obj
    }

    async function remove(id: ID, requestInit?: RequestInit): Promise<void> {
      const res = await fetch(createRequest(`${endpoint}/${id}`, requestInit, { method: 'DELETE' }))
      maybeRaiseResponse(res)
      itemMap.value.delete(id)
    }

    return {
      items,
      currentItemId,
      currentItem,
      error,
      list,
      retrieve,
      update,
      create,
      remove,
    }
  })
}