Skip to content
Snippets Groups Projects
api.ts 4.53 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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,
        }
      })
    }