Skip to content
Snippets Groups Projects
api.ts 6.17 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { defineStore } from 'pinia'
    
    import { computed, readonly, ref, 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 APIObject = { id: ID }
    
    type ExtendableAPI<T extends APIObject> = {
      itemMap: Ref<Map<ID, T>>
      endpoint: URLBuilder
      maybeRaiseResponse: (res: Response) => void
      createRequest: (
        url: string,
        customRequestData: RequestInit | undefined,
        defaultRequestData?: RequestInit | undefined,
      ) => Request
    }
    
    type APIStoreOptions<T extends APIObject> = {
      getRequestDefaults?: () => RequestInit
    
    type URLToken = string | number
    type URLBuilder = (...subPaths: URLToken[] | [...URLToken[], URLToken | URLSearchParams]) => string
    type PrefixableURLBuilder = URLBuilder & {
      prefix: (...prefixes: URLToken[]) => PrefixableURLBuilder
    }
    
    function createURLBuilder(basepath: string, useTrailingSlash = true): PrefixableURLBuilder {
    
      // Strip all trailing slashes from the basepath.
      // We handle slashes when building URLs.
      basepath = basepath.replace(/\/*$/, '')
    
    
      const buildURL: PrefixableURLBuilder = (...subPaths) => {
    
        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
      }
    
    
      buildURL.prefix = function (...prefixes: URLToken[]) {
        return createURLBuilder(buildURL(...prefixes), useTrailingSlash)
      }
    
      return buildURL
    
    }
    
    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 createExtendableAPI<T extends APIObject>(
      endpoint: URLBuilder,
      options?: APIStoreOptions<T>,
    
      const itemMap = ref<Map<ID, T>>(new Map())
      const items = computed<T[]>(() => Array.from(itemMap.value.values()))
      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 ?? {}),
        })
      }
    
      function reset() {
        itemMap.value.clear()
      }
    
    
        base: {
          error: readonly(error),
          items: readonly(items),
    
          itemMap: readonly(itemMap),
    
        createRequest,
        endpoint,
        maybeRaiseResponse,
        itemMap,
      }
    }
    
    export function APIListUnpaginated<T extends APIObject>(api: ExtendableAPI<T>) {
      async function list(requestInit?: RequestInit): Promise<T[]> {
        const res = await fetch(api.createRequest(api.endpoint(), requestInit))
        api.maybeRaiseResponse(res)
        const items: T[] = await res.json()
        for (const item of items) {
          api.itemMap.value.set(item.id, item)
    
      return { list }
    }
    export function APIRetrieve<T extends APIObject>(api: ExtendableAPI<T>) {
    
      async function retrieve(
        id: ID,
        requestInit?: RequestInit,
        options?: { useCached?: boolean },
      ): Promise<T | null> {
        if (options?.useCached && api.itemMap.value.has(id)) {
          return api.itemMap.value.get(id) as T
        }
    
    
        const res = await fetch(api.createRequest(api.endpoint(id.toString()), requestInit))
        if (res.status === 404) {
          api.itemMap.value.delete(id)
          return null
        } else {
          api.maybeRaiseResponse(res)
    
          const obj: T = await res.json()
    
          api.itemMap.value.set(obj.id, obj)
    
    export function APIUpdate<T extends APIObject>(api: ExtendableAPI<T>) {
      async function update(
        id: ID,
        data: Partial<T> | FormData,
        requestInit?: RequestInit,
      ): Promise<T> {
        const res = await fetch(
          api.createRequest(api.endpoint(id.toString()), requestInit, {
            method: 'PUT',
            headers: data instanceof FormData ? undefined : { 'Content-Type': 'application/json' },
            body: data instanceof FormData ? data : JSON.stringify(data),
          }),
        )
        api.maybeRaiseResponse(res)
        const obj: T = await res.json()
        api.itemMap.value.set(obj.id, obj)
        return obj
      }
    
      return { update }
    }
    
    export function APICreate<T extends APIObject>(api: ExtendableAPI<T>) {
      async function create(data: Partial<T> | FormData, requestInit?: RequestInit): Promise<T> {
        const res = await fetch(
          api.createRequest(api.endpoint(), requestInit, {
            method: 'POST',
            headers: data instanceof FormData ? undefined : { 'Content-Type': 'application/json' },
            body: data instanceof FormData ? data : JSON.stringify(data),
          }),
        )
        api.maybeRaiseResponse(res)
        const obj = await res.json()
        api.itemMap.value.set(obj.id, obj)
        return obj
      }
    
      return { create }
    }
    
    export function APIRemove<T extends APIObject>(api: ExtendableAPI<T>) {
      async function remove(id: ID, requestInit?: RequestInit): Promise<void> {
        const res = await fetch(
          api.createRequest(api.endpoint(id.toString()), requestInit, { method: 'DELETE' }),
        )
        api.maybeRaiseResponse(res)
        api.itemMap.value.delete(id)
      }
    
      return { remove }
    }
    
    export function createUnpaginatedAPIStore<T extends APIObject>(
      storeId: string,
      endpoint: URLBuilder,
      options?: APIStoreOptions<T>,
    ) {
      return defineStore(storeId, () => {
        const { base, ...api } = createExtendableAPI<T>(endpoint, options)
    
          ...base,
          ...APIListUnpaginated(api),
          ...APIRetrieve(api),
          ...APICreate(api),
          ...APIUpdate(api),
          ...APIRemove(api),