diff --git a/app/package.json b/app/package.json index ada105a..46054e4 100644 --- a/app/package.json +++ b/app/package.json @@ -22,18 +22,19 @@ }, "license": "FSL-1.1-MIT", "dependencies": { + "@cfworker/json-schema": "^2.0.1", "@libsql/client": "^0.14.0", - "@tanstack/react-form": "0.19.2", "@sinclair/typebox": "^0.32.34", + "@tanstack/react-form": "0.19.2", + "aws4fetch": "^1.0.18", + "dayjs": "^1.11.13", + "fast-xml-parser": "^4.4.0", + "hono": "^4.6.12", "kysely": "^0.27.4", "liquidjs": "^10.15.0", "lodash-es": "^4.17.21", - "hono": "^4.6.12", - "fast-xml-parser": "^4.4.0", - "@cfworker/json-schema": "^2.0.1", - "dayjs": "^1.11.13", "oauth4webapi": "^2.11.1", - "aws4fetch": "^1.0.18" + "swr": "^2.2.5" }, "devDependencies": { "@aws-sdk/client-s3": "^3.613.0", diff --git a/app/src/auth/api/AuthApi.ts b/app/src/auth/api/AuthApi.ts index df7ffb0..8c2fe8b 100644 --- a/app/src/auth/api/AuthApi.ts +++ b/app/src/auth/api/AuthApi.ts @@ -29,11 +29,11 @@ export class AuthApi extends ModuleApi { return res; } - async me() { + me() { return this.get<{ user: SafeUser | null }>(["me"]); } - async strategies() { + strategies() { return this.get>(["strategies"]); } diff --git a/app/src/data/api/DataApi.ts b/app/src/data/api/DataApi.ts index 0fa6d3e..967a5f1 100644 --- a/app/src/data/api/DataApi.ts +++ b/app/src/data/api/DataApi.ts @@ -15,7 +15,7 @@ export class DataApi extends ModuleApi { }; } - async readOne( + readOne( entity: string, id: PrimaryFieldType, query: Partial> = {} @@ -23,14 +23,14 @@ export class DataApi extends ModuleApi { return this.get>([entity, id], query); } - async readMany(entity: string, query: Partial = {}) { + readMany(entity: string, query: Partial = {}) { return this.get>( [entity], query ?? this.options.defaultQuery ); } - async readManyByReference( + readManyByReference( entity: string, id: PrimaryFieldType, reference: string, @@ -42,19 +42,19 @@ export class DataApi extends ModuleApi { ); } - async createOne(entity: string, input: EntityData) { + createOne(entity: string, input: EntityData) { return this.post>([entity], input); } - async updateOne(entity: string, id: PrimaryFieldType, input: EntityData) { + updateOne(entity: string, id: PrimaryFieldType, input: EntityData) { return this.patch>([entity, id], input); } - async deleteOne(entity: string, id: PrimaryFieldType) { + deleteOne(entity: string, id: PrimaryFieldType) { return this.delete>([entity, id]); } - async count(entity: string, where: RepoQuery["where"] = {}) { + count(entity: string, where: RepoQuery["where"] = {}) { return this.post>( [entity, "fn", "count"], where diff --git a/app/src/media/api/MediaApi.ts b/app/src/media/api/MediaApi.ts index c69c952..121c2fc 100644 --- a/app/src/media/api/MediaApi.ts +++ b/app/src/media/api/MediaApi.ts @@ -10,11 +10,11 @@ export class MediaApi extends ModuleApi { }; } - async getFiles() { + getFiles() { return this.get(["files"]); } - async getFile(filename: string) { + getFile(filename: string) { return this.get(["file", filename]); } @@ -32,13 +32,13 @@ export class MediaApi extends ModuleApi { }); } - async uploadFile(file: File) { + uploadFile(file: File) { const formData = new FormData(); formData.append("file", file); return this.post(["upload"], formData); } - async deleteFile(filename: string) { + deleteFile(filename: string) { return this.delete(["file", filename]); } } diff --git a/app/src/modules/ModuleApi.ts b/app/src/modules/ModuleApi.ts index 68f0627..5094abf 100644 --- a/app/src/modules/ModuleApi.ts +++ b/app/src/modules/ModuleApi.ts @@ -21,7 +21,7 @@ export type ApiResponse = { export type TInput = string | (string | number | PrimaryFieldType)[]; export abstract class ModuleApi { - fetcher = fetch; + protected fetcher?: typeof fetch; constructor(protected readonly _options: Partial = {}) {} @@ -136,17 +136,18 @@ export abstract class ModuleApi implements Promise { +export class FetchPromise> implements Promise { // @ts-ignore [Symbol.toStringTag]: "FetchPromise"; constructor( public request: Request, - protected fetcher = fetch + protected fetcher?: typeof fetch ) {} async execute() { - const res = await this.fetcher(this.request); + const fetcher = this.fetcher ?? fetch; + const res = await fetcher(this.request); let resBody: any; let resData: any; @@ -195,4 +196,9 @@ class FetchPromise implements Promise { } ); } + + getKey(): string { + const url = new URL(this.request.url); + return url.pathname + url.search; + } } diff --git a/app/src/ui/client/index.ts b/app/src/ui/client/index.ts index 30bb77f..9a22c49 100644 --- a/app/src/ui/client/index.ts +++ b/app/src/ui/client/index.ts @@ -6,5 +6,6 @@ export { useBaseUrl } from "./ClientProvider"; +export { useApi, useApiQuery } from "./use-api"; export { useAuth } from "./schema/auth/use-auth"; export { Api } from "../../Api"; diff --git a/app/src/ui/client/use-api.ts b/app/src/ui/client/use-api.ts new file mode 100644 index 0000000..e22de44 --- /dev/null +++ b/app/src/ui/client/use-api.ts @@ -0,0 +1,18 @@ +import type { Api } from "Api"; +import type { FetchPromise } from "modules/ModuleApi"; +import useSWR, { type SWRConfiguration } from "swr"; +import { useClient } from "ui/client/ClientProvider"; + +export const useApi = () => { + const client = useClient(); + return client.api; +}; + +export const useApiQuery = ( + fn: (api: Api) => FetchPromise, + options?: SWRConfiguration & { enabled?: boolean } +) => { + const api = useApi(); + const p = fn(api); + return useSWR(options?.enabled === false ? null : p.getKey(), () => p, options); +}; diff --git a/app/src/ui/routes/test/index.tsx b/app/src/ui/routes/test/index.tsx index 9cc89d4..a303ccd 100644 --- a/app/src/ui/routes/test/index.tsx +++ b/app/src/ui/routes/test/index.tsx @@ -1,5 +1,6 @@ import AppShellAccordionsTest from "ui/routes/test/tests/appshell-accordions-test"; import SwaggerTest from "ui/routes/test/tests/swagger-test"; +import SWRAndAPI from "ui/routes/test/tests/swr-and-api"; import { Route, useParams } from "wouter"; import { Empty } from "../../components/display/Empty"; import { Link } from "../../components/wouter/Link"; @@ -37,7 +38,8 @@ const tests = { EntityFieldsForm, FlowsTest, AppShellAccordionsTest, - SwaggerTest + SwaggerTest, + SWRAndAPI } as const; export default function TestRoutes() { diff --git a/app/src/ui/routes/test/tests/swr-and-api.tsx b/app/src/ui/routes/test/tests/swr-and-api.tsx new file mode 100644 index 0000000..a4808ea --- /dev/null +++ b/app/src/ui/routes/test/tests/swr-and-api.tsx @@ -0,0 +1,20 @@ +import { useState } from "react"; +import { useApiQuery } from "ui/client"; +import { Scrollable } from "ui/layouts/AppShell/AppShell"; + +export default function SWRAndAPI() { + const [enabled, setEnabled] = useState(false); + const { data, error, isLoading } = useApiQuery(({ data }) => data.readMany("posts"), { + enabled, + revalidateOnFocus: true + }); + + return ( + + + {error &&
failed to load
} + {isLoading &&
loading...
} + {data &&
{JSON.stringify(data, null, 2)}
} +
+ ); +} diff --git a/bun.lockb b/bun.lockb index 4edbd27..8a89fbc 100755 Binary files a/bun.lockb and b/bun.lockb differ