diff --git a/tools/upend_js/types.ts b/tools/upend_js/types.ts index a876d60..8ef9424 100644 --- a/tools/upend_js/types.ts +++ b/tools/upend_js/types.ts @@ -25,6 +25,21 @@ export type IValue = c: null; }; +export interface InvariantEntry { + attribute: string; + value: IValue; +} + +export type InAddress = + | Address + | { t: "Attribute" | "Url" | "Uuid"; c?: string }; + +export type InEntry = + | IEntry + | IEntry[] + | InvariantEntry + | { entity: InAddress }; + export interface ListingResult { [key: string]: IEntry; } diff --git a/webui/src/components/AddModal.svelte b/webui/src/components/AddModal.svelte index bb942b1..4f2b534 100644 --- a/webui/src/components/AddModal.svelte +++ b/webui/src/components/AddModal.svelte @@ -10,7 +10,7 @@ diff --git a/webui/src/components/Inspect.svelte b/webui/src/components/Inspect.svelte index 49124c5..0612821 100644 --- a/webui/src/components/Inspect.svelte +++ b/webui/src/components/Inspect.svelte @@ -16,6 +16,7 @@ import type { BrowseContext } from "../util/browse"; import { useParams } from "svelte-navigator"; import { GROUP_TYPE_ADDR } from "upend/constants"; + import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api"; const params = useParams(); export let address: string; @@ -119,25 +120,17 @@ const change = ev.detail; switch (change.type) { case "create": - await fetch(`api/obj`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - entity: address, - attribute: change.attribute, - value: change.value, - }), + await putEntry({ + entity: address, + attribute: change.attribute, + value: change.value, }); break; case "delete": - await fetch(`api/obj/${change.address}`, { method: "DELETE" }); + await deleteEntry(change.address); break; case "update": - await fetch(`api/obj/${address}/${change.attribute}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(change.value), - }); + await putEntityAttribute(address, change.attribute, change.value); break; default: console.error("Unimplemented AttributeChange", change); @@ -151,34 +144,30 @@ if (!groupToAdd) { return; } - await fetch(`api/obj`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify([ - { - entity: String(groupToAdd.c), - attribute: "HAS", - value: { - t: "Address", - c: address, - }, + await putEntry([ + { + entity: String(groupToAdd.c), + attribute: "HAS", + value: { + t: "Address", + c: address, }, - { - entity: String(groupToAdd.c), - attribute: "IS", - value: { - t: "Address", - c: GROUP_TYPE_ADDR, - }, + }, + { + entity: String(groupToAdd.c), + attribute: "IS", + value: { + t: "Address", + c: GROUP_TYPE_ADDR, }, - ]), - }); + }, + ]); revalidate(); groupToAdd = undefined; } async function removeGroup(address: string) { - await fetch(`api/obj/${address}`, { method: "DELETE" }); + await deleteEntry(address); revalidate(); } diff --git a/webui/src/components/display/UpObject.svelte b/webui/src/components/display/UpObject.svelte index 309f6fc..12aab83 100644 --- a/webui/src/components/display/UpObject.svelte +++ b/webui/src/components/display/UpObject.svelte @@ -5,6 +5,7 @@ import Ellipsis from "../utils/Ellipsis.svelte"; import UpLink from "./UpLink.svelte"; import { useEntity } from "../../lib/entity"; + import { nativeOpen as nativeOpenApi } from "../../lib/api"; import { readable } from "svelte/store"; import { notify, UpNotification } from "../../notifications"; import IconButton from "../utils/IconButton.svelte"; @@ -49,7 +50,7 @@ } in a default native application...` ) ); - fetch(`api/raw/${address}?native=1`) + nativeOpenApi(address) .then(async (response) => { if (!response.ok) { throw new Error(`${response.statusText} - ${await response.text()}`); diff --git a/webui/src/components/display/blobs/ImageViewer.svelte b/webui/src/components/display/blobs/ImageViewer.svelte index 8d04967..61343eb 100644 --- a/webui/src/components/display/blobs/ImageViewer.svelte +++ b/webui/src/components/display/blobs/ImageViewer.svelte @@ -1,6 +1,8 @@ diff --git a/webui/src/components/display/blobs/TextViewer.svelte b/webui/src/components/display/blobs/TextViewer.svelte index e2b6a98..1d638f5 100644 --- a/webui/src/components/display/blobs/TextViewer.svelte +++ b/webui/src/components/display/blobs/TextViewer.svelte @@ -1,15 +1,13 @@ diff --git a/webui/src/components/layout/Jobs.svelte b/webui/src/components/layout/Jobs.svelte index 9f94d08..e409353 100644 --- a/webui/src/components/layout/Jobs.svelte +++ b/webui/src/components/layout/Jobs.svelte @@ -11,18 +11,18 @@ import type { IJob } from "upend/types"; import { fade } from "svelte/transition"; import ProgessBar from "../utils/ProgessBar.svelte"; + import { fetchJobs } from "../../lib/api"; interface JobWithId extends IJob { id: string; } - let jobs: JobWithId[] = []; + let jobs: IJob[] = []; let activeJobs: JobWithId[] = []; let timeout: number; async function updateJobs() { clearTimeout(timeout); - let request = await fetch("api/jobs"); - jobs = await request.json(); + jobs = await fetchJobs(); activeJobs = Object.entries(jobs) .filter(([_, job]) => job.state == "InProgress") diff --git a/webui/src/components/utils/Selector.svelte b/webui/src/components/utils/Selector.svelte index b80178d..e33cf7b 100644 --- a/webui/src/components/utils/Selector.svelte +++ b/webui/src/components/utils/Selector.svelte @@ -2,6 +2,7 @@ import { debounce } from "lodash"; import { createEventDispatcher } from "svelte"; import type { IValue, VALUE_TYPE } from "upend/types"; + import { fetchAllAttributes } from "../../lib/api"; import { baseSearchOnce, createLabelled, @@ -53,8 +54,7 @@ switch (type) { case "attribute": { - const req = await fetch("api/all/attributes"); - const allAttributes: string[] = await req.json(); + const allAttributes = await fetchAllAttributes(); options = allAttributes .filter((attr) => attr.toLowerCase().includes(query.toLowerCase())) .map((attribute) => { diff --git a/webui/src/lib/api.ts b/webui/src/lib/api.ts new file mode 100644 index 0000000..bd7517a --- /dev/null +++ b/webui/src/lib/api.ts @@ -0,0 +1,127 @@ +import LRU from "lru-cache"; +import { UpListing, UpObject } from "upend"; +import type { + Address, + IJob, + InEntry, + IValue, + ListingResult, + PutResult, + VaultInfo +} from "upend/types"; +import type { EntityListing } from "./entity"; + +export async function fetchEntity(address: string): Promise { + const entityFetch = await fetch(`api/obj/${address}`); + const entityResult = (await entityFetch.json()) as EntityListing; + const entityListing = new UpListing(entityResult.entries); + return entityListing.getObject(address); +} + +export async function fetchEntry(address: string) { + const response = await fetch(`api/raw/${address}`); + const data = await response.json(); + const listing = new UpListing({ address: data }); + return listing.entries[0]; +} + +const queryOnceLRU = new LRU(128); +const inFlightRequests: { [key: string]: Promise } = {}; + +export async function queryOnce(query: string): Promise { + const cacheResult = queryOnceLRU.get(query); + if (!cacheResult) { + if (!inFlightRequests[query]) { + console.debug(`Querying: ${query}`); + inFlightRequests[query] = new Promise((resolve, reject) => { + fetch("api/query", { method: "POST", body: query, keepalive: true }) + .then(async (response) => { + resolve(new UpListing(await response.json())); + }) + .catch((err) => reject(err)); + }); + } else { + console.debug(`Chaining request for ${query}...`); + } + return await inFlightRequests[query]; + } else { + console.debug(`Returning cached: ${query}`); + return cacheResult; + } +} + +export async function putEntry(entry: InEntry): Promise { + const response = await fetch(`api/obj`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(entry), + }); + + return await response.json(); +} + +export async function putEntityAttribute( + entity: Address, + attribute: string, + value: IValue +): Promise
{ + const response = await fetch(`api/obj/${entity}/${attribute}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(value), + }); + + return await response.json(); +} + +export async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append("file", file); + + const response = await fetch("api/obj", { + method: "PUT", + body: formData, + }); + + if (!response.ok) { + throw Error(await response.text()); + } + + return await response.json(); +} + +export async function deleteEntry(address: Address): Promise { + await fetch(`api/obj/${address}`, { method: "DELETE" }); +} + +export async function getRaw(address: Address, preview = false) { + return await fetch(`api/${preview ? "thumb" : "raw"}/${address}`); +} + +export async function refreshVault() { + return await fetch("api/refresh", { method: "POST" }); +} + +export async function nativeOpen(address: Address) { + return fetch(`api/raw/${address}?native=1`); +} + +export async function fetchRoots(): Promise { + const response = await fetch("api/hier_roots"); + return await response.json(); +} + +export async function fetchJobs(): Promise { + const response = await fetch("api/jobs"); + return await response.json(); +} + +export async function fetchAllAttributes(): Promise { + const response = await fetch("api/all/attributes"); + return await response.json(); +} + +export async function fetchInfo(): Promise { + const response = await fetch("api/info"); + return await response.json(); +} diff --git a/webui/src/lib/entity.ts b/webui/src/lib/entity.ts index 605f6a8..a52f472 100644 --- a/webui/src/lib/entity.ts +++ b/webui/src/lib/entity.ts @@ -1,5 +1,4 @@ // import { useSWR } from "sswr"; -import LRU from "lru-cache"; import { derived, Readable } from "svelte/store"; import { UpListing, UpObject } from "upend"; import type { ListingResult } from "upend/types"; @@ -19,9 +18,6 @@ export interface EntityListing { entries: ListingResult; } -const queryOnceLRU = new LRU(128); -const inFlightRequests: { [key: string]: Promise } = {}; - export function useEntity(address: string) { const { data, error, revalidate } = useSWR( `api/obj/${address}` @@ -51,20 +47,6 @@ export function useEntity(address: string) { }; } -export async function fetchEntity(address: string): Promise { - const entityFetch = await fetch(`api/obj/${address}`); - const entityResult = (await entityFetch.json()) as EntityListing; - const entityListing = new UpListing(entityResult.entries); - return entityListing.getObject(address); -} - -export async function fetchEntry(address: string) { - const response = await fetch(`api/raw/${address}`); - const data = await response.json(); - const listing = new UpListing({ address: data }); - return listing.entries[0]; -} - export function query(query: string) { console.debug(`Querying: ${query}`); const { data, error, revalidate } = useSWR( @@ -82,25 +64,3 @@ export function query(query: string) { revalidate, }; } - -export async function queryOnce(query: string): Promise { - const cacheResult = queryOnceLRU.get(query); - if (!cacheResult) { - if (!inFlightRequests[query]) { - console.debug(`Querying: ${query}`); - inFlightRequests[query] = new Promise((resolve, reject) => { - fetch("api/query", { method: "POST", body: query, keepalive: true }) - .then(async (response) => { - resolve(new UpListing(await response.json())); - }) - .catch((err) => reject(err)); - }); - } else { - console.debug(`Chaining request for ${query}...`); - } - return await inFlightRequests[query]; - } else { - console.debug(`Returning cached: ${query}`); - return cacheResult; - } -} diff --git a/webui/src/util/info.ts b/webui/src/util/info.ts index 5c3c477..1582733 100644 --- a/webui/src/util/info.ts +++ b/webui/src/util/info.ts @@ -1,8 +1,9 @@ import { readable, Readable } from "svelte/store"; import type { VaultInfo } from "upend/types"; +import { fetchInfo } from "../lib/api"; export const vaultInfo: Readable = readable(undefined, (set) => { - fetch("api/info").then(async (response) => { - set(await response.json()); + fetchInfo().then(async (info) => { + set(info); }); }); diff --git a/webui/src/util/search.ts b/webui/src/util/search.ts index 56ade8e..5f9e9b7 100644 --- a/webui/src/util/search.ts +++ b/webui/src/util/search.ts @@ -1,6 +1,7 @@ import type { UpEntry } from "upend"; -import type { PutResult } from "upend/types"; -import { query as queryFn, queryOnce } from "../lib/entity"; +import type { InEntry } from "upend/types"; +import { putEntry, queryOnce } from "../lib/api"; +import { query as queryFn } from "../lib/entity"; export function baseSearch(query: string) { return queryFn( @@ -25,7 +26,7 @@ export async function getObjects( } export async function createLabelled(label: string) { - let body: unknown; + let body: InEntry; if (label.match("^[\\w]+://[\\w]")) { body = { entity: { @@ -43,16 +44,10 @@ export async function createLabelled(label: string) { }; } - const response = await fetch(`api/obj`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - throw new Error(`Failed to create object: ${await response.text()}`); + try { + const [_, entry] = await putEntry(body); + return entry; + } catch (error) { + throw new Error(`Failed to create object: ${error}`); } - - const [_, entry] = (await response.json()) as PutResult; - return entry; } diff --git a/webui/src/views/Home.svelte b/webui/src/views/Home.svelte index 83ca0c0..d116d47 100644 --- a/webui/src/views/Home.svelte +++ b/webui/src/views/Home.svelte @@ -1,19 +1,16 @@