added useApi and useApiQuery to work seamlessly with SWR (tbc)

This commit is contained in:
dswbx
2024-12-12 10:37:52 +01:00
parent 43ec075a32
commit 9d9aa7b7a5
10 changed files with 72 additions and 24 deletions

View File

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

View File

@@ -29,11 +29,11 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
return res;
}
async me() {
me() {
return this.get<{ user: SafeUser | null }>(["me"]);
}
async strategies() {
strategies() {
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
}

View File

@@ -15,7 +15,7 @@ export class DataApi extends ModuleApi<DataApiOptions> {
};
}
async readOne(
readOne(
entity: string,
id: PrimaryFieldType,
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
@@ -23,14 +23,14 @@ export class DataApi extends ModuleApi<DataApiOptions> {
return this.get<RepositoryResponse<EntityData>>([entity, id], query);
}
async readMany(entity: string, query: Partial<RepoQuery> = {}) {
readMany(entity: string, query: Partial<RepoQuery> = {}) {
return this.get<Pick<RepositoryResponse, "meta" | "data">>(
[entity],
query ?? this.options.defaultQuery
);
}
async readManyByReference(
readManyByReference(
entity: string,
id: PrimaryFieldType,
reference: string,
@@ -42,19 +42,19 @@ export class DataApi extends ModuleApi<DataApiOptions> {
);
}
async createOne(entity: string, input: EntityData) {
createOne(entity: string, input: EntityData) {
return this.post<RepositoryResponse<EntityData>>([entity], input);
}
async updateOne(entity: string, id: PrimaryFieldType, input: EntityData) {
updateOne(entity: string, id: PrimaryFieldType, input: EntityData) {
return this.patch<RepositoryResponse<EntityData>>([entity, id], input);
}
async deleteOne(entity: string, id: PrimaryFieldType) {
deleteOne(entity: string, id: PrimaryFieldType) {
return this.delete<RepositoryResponse<EntityData>>([entity, id]);
}
async count(entity: string, where: RepoQuery["where"] = {}) {
count(entity: string, where: RepoQuery["where"] = {}) {
return this.post<RepositoryResponse<{ entity: string; count: number }>>(
[entity, "fn", "count"],
where

View File

@@ -10,11 +10,11 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
};
}
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<MediaApiOptions> {
});
}
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]);
}
}

View File

@@ -21,7 +21,7 @@ export type ApiResponse<Data = any> = {
export type TInput = string | (string | number | PrimaryFieldType)[];
export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModuleApiOptions> {
fetcher = fetch;
protected fetcher?: typeof fetch;
constructor(protected readonly _options: Partial<Options> = {}) {}
@@ -136,17 +136,18 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
}
}
class FetchPromise<T> implements Promise<T> {
export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
// @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<T> implements Promise<T> {
}
);
}
getKey(): string {
const url = new URL(this.request.url);
return url.pathname + url.search;
}
}

View File

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

View File

@@ -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 = <T = any>(
fn: (api: Api) => FetchPromise<T>,
options?: SWRConfiguration & { enabled?: boolean }
) => {
const api = useApi();
const p = fn(api);
return useSWR<T>(options?.enabled === false ? null : p.getKey(), () => p, options);
};

View File

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

View File

@@ -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 (
<Scrollable>
<button onClick={() => setEnabled((p) => !p)}>{enabled ? "disable" : "enable"}</button>
{error && <div>failed to load</div>}
{isLoading && <div>loading...</div>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</Scrollable>
);
}

BIN
bun.lockb

Binary file not shown.