refactor: typesafe api call (#16)

This commit is contained in:
Stephen Zhou 2024-05-27 13:15:55 +08:00 committed by GitHub
parent b10280712a
commit 83befff7f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 779 additions and 18 deletions

View File

@ -12,6 +12,7 @@ export default defineConfig(
arrowParens: true,
braceStyle: "1tbs",
},
ignores: ["src/renderer/src/hono.ts"],
},
{
rules: {

734
src/renderer/src/hono.ts Normal file
View File

@ -0,0 +1,734 @@
import * as hono_hono_base from 'hono/hono-base';
import * as hono_utils_http_status from 'hono/utils/http-status';
import * as hono from 'hono';
declare const routes: hono_hono_base.HonoBase<hono.Env, {
"/reads": {
$post: {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$delete: {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
} & {
"/collections": {
$get: {
input: {
query: {
entryId: string;
};
};
output: {
code: 0;
data: boolean;
};
outputFormat: "json";
status: 200;
} | {
input: {
query: {
entryId: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$post: {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$delete: {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
entryId: string;
csrfToken: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
} & {
"/categories": {
$get: {
input: {
query: {
view?: string | undefined;
};
};
output: {
code: 0;
data?: string[] | undefined;
};
outputFormat: "json";
status: 200;
} | {
input: {
query: {
view?: string | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$delete: {
input: {
json: {
feedIdList: string[];
deleteSubscriptions: boolean;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
feedIdList: string[];
deleteSubscriptions: boolean;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$patch: {
input: {
json: {
category: string;
feedIdList: string[];
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
category: string;
feedIdList: string[];
};
};
output: {
code: 2;
error: string;
};
outputFormat: "json";
status: 400;
} | {
input: {
json: {
category: string;
feedIdList: string[];
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
} & {
"/feeds/refresh": {
$get: {
input: {
query: {
id: string;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
query: {
id: string;
};
};
output: {
code: 2;
error: "Feed not found";
};
outputFormat: "json";
status: 400;
} | {
input: {
query: {
id: string;
};
};
output: {
code: 1;
error: "Unauthorized" | "Only admins can refresh feeds";
};
outputFormat: "json";
status: 401;
};
};
} & {
"/discover": {
$post: {
input: {
json: {
keyword: string;
type?: "auto" | "rss" | undefined;
};
};
output: {
code: 0;
data: {
feed: {
description: string | null;
title: string | null;
id: string;
image: string | null;
url: string;
siteUrl: string | null;
checkedAt: Date;
nextCheckAt: Date;
lastModifiedHeader: string | null;
etagHeader: string | null;
ttl: number | null;
errorMessage: string | null;
errorAt: Date | null;
};
entries?: {
description: string | null;
title: string | null;
content: string | null;
id: string;
url: string | null;
feedId: string;
guid: string;
author: string | null;
changedAt: Date;
publishedAt: Date;
images: string | null;
categories: string | null;
}[] | undefined;
docs?: string | undefined;
isSubscribed?: boolean | undefined;
subscriptionCount?: number | undefined;
}[];
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
keyword: string;
type?: "auto" | "rss" | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
} & {
"/auth-app/new-session": {
$post: {
input: {
json: {
csrfToken: string;
};
};
output: {
code: 0;
data: {
userId: string;
sessionToken: string;
expires: string;
};
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
csrfToken: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
} | {
input: {
json: {
csrfToken: string;
};
};
output: {
code: 1;
error: "Failed to create session";
};
outputFormat: "json";
status: 500;
};
};
"/auth-app/update-account": {
$patch: {
input: {
json: {
csrfToken: string;
name?: string | null | undefined;
image?: string | null | undefined;
handle?: string | null | undefined;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
csrfToken: string;
name?: string | null | undefined;
image?: string | null | undefined;
handle?: string | null | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
} & {
[x: `/import/${string}`]: {
[x: `$${Lowercase<string>}`]: {
input: Partial<hono.ValidationTargets>;
output: any;
outputFormat: string;
status: hono_utils_http_status.StatusCode;
};
};
} & {
"/entries": {
$post: {
input: {
json: {
feedId?: string | undefined;
view?: number | undefined;
category?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
feedIdList?: string[] | undefined;
};
};
output: {
code: 0;
total: number;
data?: {
description: string | null;
title: string | null;
id: string;
feeds: {
description: string | null;
title: string | null;
id: string;
image: string | null;
url: string;
siteUrl: string | null;
checkedAt: Date;
nextCheckAt: Date;
lastModifiedHeader: string | null;
etagHeader: string | null;
ttl: number | null;
errorMessage: string | null;
errorAt: Date | null;
};
url: string | null;
feedId: string;
guid: string;
author: string | null;
changedAt: Date;
publishedAt: Date;
images: string | null;
categories: string | null;
collected: boolean;
read: boolean;
}[] | undefined;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
feedId?: string | undefined;
view?: number | undefined;
category?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
feedIdList?: string[] | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$get: {
input: {
query: {
id: string;
};
};
output: {
code: 0;
data?: {
description: string | null;
title: string | null;
content: string | null;
id: string;
feeds: {
description: string | null;
title: string | null;
id: string;
image: string | null;
url: string;
siteUrl: string | null;
checkedAt: Date;
nextCheckAt: Date;
lastModifiedHeader: string | null;
etagHeader: string | null;
ttl: number | null;
errorMessage: string | null;
errorAt: Date | null;
};
url: string | null;
feedId: string;
guid: string;
author: string | null;
changedAt: Date;
publishedAt: Date;
images: string | null;
categories: string | null;
collected: boolean;
read: boolean;
} | undefined;
};
outputFormat: "json";
status: 200;
} | {
input: {
query: {
id: string;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
} & {
"/subscriptions": {
$get: {
input: {
query: {
view?: string | undefined;
};
};
output: {
code: 0;
data?: {
title: string | null;
userId: string;
feeds: {
description: string | null;
title: string | null;
id: string;
image: string | null;
url: string;
siteUrl: string | null;
checkedAt: Date;
nextCheckAt: Date;
lastModifiedHeader: string | null;
etagHeader: string | null;
ttl: number | null;
errorMessage: string | null;
errorAt: Date | null;
};
feedId: string;
view: number;
category: string | null;
isPrivate: boolean | null;
}[] | undefined;
};
outputFormat: "json";
status: 200;
} | {
input: {
query: {
view?: string | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$post: {
input: {
json: {
url: string;
view: number;
csrfToken: string;
category?: string | null | undefined;
isPrivate?: boolean | null | undefined;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
url: string;
view: number;
csrfToken: string;
category?: string | null | undefined;
isPrivate?: boolean | null | undefined;
};
};
output: {
code: 2;
error: string;
};
outputFormat: "json";
status: 400;
} | {
input: {
json: {
url: string;
view: number;
csrfToken: string;
category?: string | null | undefined;
isPrivate?: boolean | null | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$delete: {
input: {
json: {
csrfToken: string;
url?: string | undefined;
feedId?: string | undefined;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
csrfToken: string;
url?: string | undefined;
feedId?: string | undefined;
};
};
output: {
code: 2;
error: string;
};
outputFormat: "json";
status: 400;
} | {
input: {
json: {
csrfToken: string;
url?: string | undefined;
feedId?: string | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
$patch: {
input: {
json: {
feedId: string;
view: number;
csrfToken: string;
category?: string | null | undefined;
isPrivate?: boolean | null | undefined;
};
};
output: {
code: 0;
};
outputFormat: "json";
status: 200;
} | {
input: {
json: {
feedId: string;
view: number;
csrfToken: string;
category?: string | null | undefined;
isPrivate?: boolean | null | undefined;
};
};
output: {
code: 2;
error: string;
};
outputFormat: "json";
status: 400;
} | {
input: {
json: {
feedId: string;
view: number;
csrfToken: string;
category?: string | null | undefined;
isPrivate?: boolean | null | undefined;
};
};
output: {
code: 1;
error: "Unauthorized";
};
outputFormat: "json";
status: 401;
};
};
}, "/">;
type AppType = typeof routes;
export type { AppType };

View File

@ -1,3 +1,6 @@
import type { apiClient } from "@renderer/queries/api-fetch"
import type { InferResponseType } from "hono/client"
export type ActiveList = {
level: string
id: string | number
@ -22,16 +25,15 @@ export type FeedResponse = {
errorMessage?: string
}
export type SubscriptionResponse = {
userId: string
feedId: string
view: number
category?: string
title?: string
isPrivate?: boolean
unread?: number
feeds: FeedResponse
}[]
export type SubscriptionResponse = Array<
Exclude<
Extract<
InferResponseType<typeof apiClient.subscriptions.$get>,
{ code: 0 }
>["data"],
undefined
>[number] & { unread?: number }
>
export type EntriesResponse = {
author?: string

View File

@ -1,4 +1,6 @@
import { getCsrfToken } from "@hono/auth-js/react"
import type { AppType } from "@renderer/hono"
import { hc } from "hono/client"
import { ofetch } from "ofetch"
export const apiFetch = ofetch.create({
@ -18,3 +20,27 @@ export const apiFetch = ofetch.create({
}
},
})
export const apiClient = hc<AppType>(
import.meta.env.VITE_API_URL,
{
fetch: async (...args) => {
const options = args[1] as RequestInit
options.credentials = "include"
if (options.method && options.method.toLowerCase() !== "get") {
const csrfToken = await getCsrfToken()
if (!options.body) {
options.body = JSON.stringify({})
}
if (options.body instanceof FormData) {
options.body.append("csrfToken", csrfToken)
} else {
if (typeof options.body === "string") {
options.body = JSON.stringify({ csrfToken, ...JSON.parse(options.body) })
}
}
}
return fetch(args[0], options)
},
},
)

View File

@ -1,5 +1,5 @@
import type { SubscriptionResponse } from "@renderer/lib/types"
import { apiFetch } from "@renderer/queries/api-fetch"
import { apiClient, apiFetch } from "@renderer/queries/api-fetch"
import { useQuery } from "@tanstack/react-query"
import { parse } from "tldts"
@ -16,13 +16,11 @@ export const useSubscriptions = (view?: number) =>
useQuery({
queryKey: ["subscriptions", view],
queryFn: async () => {
const { data: subscriptions } = await apiFetch<{
data: SubscriptionResponse
}>("/subscriptions", {
query: {
view,
},
})
const res = await (await apiClient.subscriptions.$get({ query: { view: String(view) } })).json()
if (res.code === 1) {
throw new Error(res.error)
}
const subscriptions = res.data as SubscriptionResponse
const categories = {
list: {},