Newer
Older
import { defineStore } from 'pinia'
import { computed, 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 ?? {}),
})
}
return {
base: { items, error },
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 items
}
return { list }
}
export function APIRetrieve<T extends APIObject>(api: ExtendableAPI<T>) {
async function retrieve(id: ID, requestInit?: RequestInit): Promise<T | null> {
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)
return { retrieve }
}
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
}
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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),