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 } 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 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, } }) }