[ui] refactor - centralize all `fetch()` calls in `api.ts`

feat/vaults
Tomáš Mládek 2022-02-20 13:06:01 +01:00
parent c2b26ccfee
commit e522e99209
No known key found for this signature in database
GPG Key ID: 65E225C8B3E2ED8A
16 changed files with 249 additions and 193 deletions

View File

@ -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;
}

View File

@ -10,7 +10,7 @@
<script lang="ts">
import { useNavigate } from "svelte-navigator";
import type { PutResult } from "upend/types";
import { uploadFile } from "../lib/api";
import Icon from "./utils/Icon.svelte";
import IconButton from "./utils/IconButton.svelte";
const navigate = useNavigate();
@ -30,21 +30,7 @@
try {
const responses = await Promise.all(
files.map(async (file) => {
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()) as PutResult;
})
files.map(async (file) => uploadFile(file))
);
const addresses = responses.map(([_, entry]) => entry);

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { putEntityAttribute } from "../lib/api";
import { normUrl } from "../util/history";
import Inspect from "./Inspect.svelte";
@ -19,10 +20,9 @@
window.open(normUrl(`/browse/${address}`), "_blank");
}
$: fetch(`api/obj/${address}/LAST_VISITED`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ t: "Number", c: new Date().getTime() / 1000 }),
$: putEntityAttribute(address, "LAST_VISITED", {
t: "Number",
c: new Date().getTime() / 1000,
});
</script>

View File

@ -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();
}
</script>

View File

@ -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()}`);

View File

@ -1,6 +1,8 @@
<script lang="ts">
import type { PutResult } from "upend/types";
import { fetchEntity, useEntity } from "../../../lib/entity";
import type { IEntry } from "upend/types";
import { deleteEntry, fetchEntity, putEntry } from "../../../lib/api";
import { useEntity } from "../../../lib/entity";
import Spinner from "../../utils/Spinner.svelte";
import UpObject from "../UpObject.svelte";
@ -98,87 +100,71 @@
});
anno.on("createAnnotation", async (annotation) => {
const lensUuidFetch = await fetch("api/obj", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
entity: {
t: "Uuid",
},
}),
const [_, uuid] = await putEntry({
entity: {
t: "Uuid",
},
});
const [_, uuid] = (await lensUuidFetch.json()) as PutResult;
await fetch("api/obj", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([
{
entity: uuid,
attribute: "ANNOTATES",
value: {
t: "Address",
c: address,
},
await putEntry([
{
entity: uuid,
attribute: "ANNOTATES",
value: {
t: "Address",
c: address,
},
{
},
{
entity: uuid,
attribute: "W3C_FRAGMENT_SELECTOR",
value: {
t: "String",
c: annotation.target.selector.value,
},
},
...annotation.body.map((body) => {
return {
entity: uuid,
attribute: "W3C_FRAGMENT_SELECTOR",
attribute: "LBL",
value: {
t: "String",
c: annotation.target.selector.value,
c: body.value,
},
},
...annotation.body.map((body) => {
return {
entity: uuid,
attribute: "LBL",
value: {
t: "String",
c: body.value,
},
};
}),
]),
});
} as IEntry;
}),
]);
});
anno.on("updateAnnotation", async (annotation) => {
const annotationObject = await fetchEntity(annotation.id);
await Promise.all(
annotationObject.attr["LBL"]
.concat(annotationObject.attr["W3C_FRAGMENT_SELECTOR"])
.map(async (e) => {
fetch(`api/obj/${e.address}`, { method: "DELETE" });
})
.map(async (e) => deleteEntry(e.address))
);
await fetch("api/obj", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([
{
await putEntry([
{
entity: annotation.id,
attribute: "W3C_FRAGMENT_SELECTOR",
value: {
t: "String",
c: annotation.target.selector.value,
},
},
...annotation.body.map((body) => {
return {
entity: annotation.id,
attribute: "W3C_FRAGMENT_SELECTOR",
attribute: "LBL",
value: {
t: "String",
c: annotation.target.selector.value,
c: body.value,
},
},
...annotation.body.map((body) => {
return {
entity: annotation.id,
attribute: "LBL",
value: {
t: "String",
c: body.value,
},
};
}),
]),
});
} as IEntry;
}),
]);
});
anno.on("deleteAnnotation", async (annotation) => {
await fetch(`api/obj/${annotation.id}`, {
method: "DELETE",
});
await deleteEntry(annotation.id);
});
}
</script>

View File

@ -1,15 +1,13 @@
<script lang="ts">
import { getRaw } from "../../../lib/api";
import IconButton from "../../utils/IconButton.svelte";
import Spinner from "../../utils/Spinner.svelte";
export let address: string;
let mode: "preview" | "full" | "markdown" = "preview";
$: textContent = (async () => {
const response = await fetch(
`api/${mode == "preview" ? "thumb" : "raw"}/${address}`
);
const response = await getRaw(address, mode == "preview");
const text = await response.text();
if (mode === "markdown") {
const { marked } = await import("marked");

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { Link, useLocation, useNavigate } from "svelte-navigator";
import { useMatch } from "svelte-navigator";
import { refreshVault } from "../../lib/api";
import { addEmitter } from "../AddModal.svelte";
import Icon from "../utils/Icon.svelte";
import Input from "../utils/Input.svelte";
@ -34,7 +35,7 @@
}
async function rescan() {
await fetch("api/refresh", { method: "POST" });
refreshVault();
jobsEmitter.emit("reload");
}
</script>

View File

@ -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")

View File

@ -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) => {

127
webui/src/lib/api.ts Normal file
View File

@ -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<UpObject> {
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<string, UpListing>(128);
const inFlightRequests: { [key: string]: Promise<UpListing> } = {};
export async function queryOnce(query: string): Promise<UpListing> {
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<PutResult> {
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<Address> {
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<PutResult> {
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<void> {
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<ListingResult> {
const response = await fetch("api/hier_roots");
return await response.json();
}
export async function fetchJobs(): Promise<IJob[]> {
const response = await fetch("api/jobs");
return await response.json();
}
export async function fetchAllAttributes(): Promise<string[]> {
const response = await fetch("api/all/attributes");
return await response.json();
}
export async function fetchInfo(): Promise<VaultInfo> {
const response = await fetch("api/info");
return await response.json();
}

View File

@ -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<string, UpListing>(128);
const inFlightRequests: { [key: string]: Promise<UpListing> } = {};
export function useEntity(address: string) {
const { data, error, revalidate } = useSWR<EntityListing, unknown>(
`api/obj/${address}`
@ -51,20 +47,6 @@ export function useEntity(address: string) {
};
}
export async function fetchEntity(address: string): Promise<UpObject> {
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<ListingResult, unknown>(
@ -82,25 +64,3 @@ export function query(query: string) {
revalidate,
};
}
export async function queryOnce(query: string): Promise<UpListing> {
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;
}
}

View File

@ -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<VaultInfo> = readable(undefined, (set) => {
fetch("api/info").then(async (response) => {
set(await response.json());
fetchInfo().then(async (info) => {
set(info);
});
});

View File

@ -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;
}

View File

@ -1,19 +1,16 @@
<script lang="ts">
import _ from "lodash";
import { UpListing } from "upend";
import type { ListingResult } from "upend/types";
import AttributeView from "../components/AttributeView.svelte";
import UpObjectCard from "../components/display/UpObjectCard.svelte";
import Spinner from "../components/utils/Spinner.svelte";
import { fetchRoots } from "../lib/api";
import { query } from "../lib/entity";
import { UpType } from "../lib/types";
import { vaultInfo } from "../util/info";
import { updateTitle } from "../util/title";
const roots = (async () => {
const response = await fetch("api/hier_roots");
const data = (await response.json()) as ListingResult;
const data = await fetchRoots();
const listing = new UpListing(data);
return Object.values(listing.objects)
.filter((obj) => Boolean(obj.attr["LBL"]))

View File

@ -4669,8 +4669,8 @@ __metadata:
"upend@file:../tools/upend_js::locator=svelte-app%40workspace%3A.":
version: 0.0.1
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=fd602e&locator=svelte-app%40workspace%3A."
checksum: 14cc9cfb6f04a85ec4715b3bbb6b5636b898ccb1ac2418cae9d5e8c00ec25f6245e5be1f8f831c8f24aa3cc0ecb97942c1a569a2fdee104383beb173b40daf0d
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=d7ca1d&locator=svelte-app%40workspace%3A."
checksum: 50dc93a08980c68982a46c1c7f59bac5d5e000f2d88a4b8167b4353bafc99c3706375775e78f9fc685a8e03e51cc6c3403714ce4c57ebe5254776dba1ff584b7
languageName: node
linkType: hard