diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/api/package.json | 2 | ||||
| -rw-r--r-- | packages/benchmarks/package.json | 2 | ||||
| -rw-r--r-- | packages/e2e_tests/package.json | 2 | ||||
| -rw-r--r-- | packages/shared-react/hooks/assets.ts | 99 | ||||
| -rw-r--r-- | packages/shared-react/hooks/bookmarks.ts | 250 | ||||
| -rw-r--r-- | packages/shared-react/hooks/highlights.ts | 93 | ||||
| -rw-r--r-- | packages/shared-react/hooks/lists.ts | 196 | ||||
| -rw-r--r-- | packages/shared-react/hooks/reader-settings.tsx | 49 | ||||
| -rw-r--r-- | packages/shared-react/hooks/rules.ts | 69 | ||||
| -rw-r--r-- | packages/shared-react/hooks/tags.ts | 203 | ||||
| -rw-r--r-- | packages/shared-react/hooks/users.ts | 56 | ||||
| -rw-r--r-- | packages/shared-react/package.json | 3 | ||||
| -rw-r--r-- | packages/shared-react/providers/trpc-provider.tsx | 18 | ||||
| -rw-r--r-- | packages/shared-react/trpc.ts | 4 | ||||
| -rw-r--r-- | packages/trpc/package.json | 2 |
15 files changed, 634 insertions, 414 deletions
diff --git a/packages/api/package.json b/packages/api/package.json index e49204b9..676f64b0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -21,7 +21,7 @@ "@karakeep/shared": "workspace:*", "@karakeep/shared-server": "workspace:*", "@karakeep/trpc": "workspace:*", - "@trpc/server": "^11.4.3", + "@trpc/server": "^11.9.0", "drizzle-orm": "^0.44.2", "file-type": "^21.2.0", "hono": "^4.10.6", diff --git a/packages/benchmarks/package.json b/packages/benchmarks/package.json index 52862862..ce768c43 100644 --- a/packages/benchmarks/package.json +++ b/packages/benchmarks/package.json @@ -15,7 +15,7 @@ "dependencies": { "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", + "@trpc/client": "^11.9.0", "p-limit": "^7.2.0", "superjson": "^2.2.1", "tinybench": "^6.0.0", diff --git a/packages/e2e_tests/package.json b/packages/e2e_tests/package.json index 45d512bb..d93318aa 100644 --- a/packages/e2e_tests/package.json +++ b/packages/e2e_tests/package.json @@ -19,7 +19,7 @@ "@karakeep/sdk": "workspace:*", "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", + "@trpc/client": "^11.9.0", "superjson": "^2.2.1", "zod": "^3.24.2" }, diff --git a/packages/shared-react/hooks/assets.ts b/packages/shared-react/hooks/assets.ts index 5367e97c..8be21304 100644 --- a/packages/shared-react/hooks/assets.ts +++ b/packages/shared-react/hooks/assets.ts @@ -1,49 +1,74 @@ -import { api } from "../trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useAttachBookmarkAsset( - ...opts: Parameters<typeof api.assets.attachAsset.useMutation> + opts?: Parameters<TRPCApi["assets"]["attachAsset"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.assets.attachAsset.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.assets.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.assets.attachAsset.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.assets.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useReplaceBookmarkAsset( - ...opts: Parameters<typeof api.assets.replaceAsset.useMutation> + opts?: Parameters<TRPCApi["assets"]["replaceAsset"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.assets.replaceAsset.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.assets.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.assets.replaceAsset.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.assets.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDetachBookmarkAsset( - ...opts: Parameters<typeof api.assets.detachAsset.useMutation> + opts?: Parameters<TRPCApi["assets"]["detachAsset"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.assets.detachAsset.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.assets.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.assets.detachAsset.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.assets.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts index aea2d185..6ff75d7e 100644 --- a/packages/shared-react/hooks/bookmarks.ts +++ b/packages/shared-react/hooks/bookmarks.ts @@ -1,132 +1,196 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + import { getBookmarkRefreshInterval } from "@karakeep/shared/utils/bookmarkUtils"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; import { useBookmarkGridContext } from "./bookmark-grid-context"; import { useAddBookmarkToList } from "./lists"; +type TRPCApi = ReturnType<typeof useTRPC>; + export function useAutoRefreshingBookmarkQuery( - input: Parameters<typeof api.bookmarks.getBookmark.useQuery>[0], + input: Parameters<TRPCApi["bookmarks"]["getBookmark"]["queryOptions"]>[0], ) { - return api.bookmarks.getBookmark.useQuery(input, { - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); - }, - }); + const api = useTRPC(); + return useQuery( + api.bookmarks.getBookmark.queryOptions(input, { + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }), + ); } export function useCreateBookmark( - ...opts: Parameters<typeof api.bookmarks.createBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["createBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.createBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.createBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useCreateBookmarkWithPostHook( - ...opts: Parameters<typeof api.bookmarks.createBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["createBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const postCreationCB = useBookmarkPostCreationHook(); - return api.bookmarks.createBookmark.useMutation({ - ...opts[0], - onSuccess: async (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - await postCreationCB(res.id); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + return useMutation( + api.bookmarks.createBookmark.mutationOptions({ + ...opts, + onSuccess: async (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + await postCreationCB(res.id); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteBookmark( - ...opts: Parameters<typeof api.bookmarks.deleteBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["deleteBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.deleteBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.deleteBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateBookmark( - ...opts: Parameters<typeof api.bookmarks.updateBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["updateBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.updateBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.updateBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useSummarizeBookmark( - ...opts: Parameters<typeof api.bookmarks.summarizeBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["summarizeBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.summarizeBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.summarizeBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useRecrawlBookmark( - ...opts: Parameters<typeof api.bookmarks.recrawlBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["recrawlBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.recrawlBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.recrawlBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateBookmarkTags( - ...opts: Parameters<typeof api.bookmarks.updateTags.useMutation> + opts?: Parameters<TRPCApi["bookmarks"]["updateTags"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.updateTags.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - - [...res.attached, ...res.detached].forEach((id) => { - apiUtils.tags.get.invalidate({ tagId: id }); - apiUtils.bookmarks.getBookmarks.invalidate({ tagId: id }); - }); - apiUtils.tags.list.invalidate(); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.updateTags.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + + [...res.attached, ...res.detached].forEach((id) => { + queryClient.invalidateQueries( + api.tags.get.queryFilter({ tagId: id }), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ tagId: id }), + ); + }); + queryClient.invalidateQueries(api.tags.list.pathFilter()); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } /** diff --git a/packages/shared-react/hooks/highlights.ts b/packages/shared-react/hooks/highlights.ts index e642f878..3f6a6e01 100644 --- a/packages/shared-react/hooks/highlights.ts +++ b/packages/shared-react/hooks/highlights.ts @@ -1,49 +1,68 @@ -import { api } from "../trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useCreateHighlight( - ...opts: Parameters<typeof api.highlights.create.useMutation> + opts?: Parameters<TRPCApi["highlights"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.highlights.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.highlights.getForBookmark.invalidate({ - bookmarkId: req.bookmarkId, - }); - apiUtils.highlights.getAll.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.highlights.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.highlights.getForBookmark.queryFilter({ + bookmarkId: req.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateHighlight( - ...opts: Parameters<typeof api.highlights.update.useMutation> + opts?: Parameters<TRPCApi["highlights"]["update"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.highlights.update.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.highlights.getForBookmark.invalidate({ - bookmarkId: res.bookmarkId, - }); - apiUtils.highlights.getAll.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.highlights.update.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.highlights.getForBookmark.queryFilter({ + bookmarkId: res.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteHighlight( - ...opts: Parameters<typeof api.highlights.delete.useMutation> + opts?: Parameters<TRPCApi["highlights"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.highlights.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.highlights.getForBookmark.invalidate({ - bookmarkId: res.bookmarkId, - }); - apiUtils.highlights.getAll.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.highlights.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.highlights.getForBookmark.queryFilter({ + bookmarkId: res.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts index d269efe3..231e43a7 100644 --- a/packages/shared-react/hooks/lists.ts +++ b/packages/shared-react/hooks/lists.ts @@ -1,113 +1,155 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + import { ZBookmarkList } from "@karakeep/shared/types/lists"; import { listsToTree, ZBookmarkListRoot, } from "@karakeep/shared/utils/listUtils"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useCreateBookmarkList( - ...opts: Parameters<typeof api.lists.create.useMutation> + opts?: Parameters<TRPCApi["lists"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useEditBookmarkList( - ...opts: Parameters<typeof api.lists.edit.useMutation> + opts?: Parameters<TRPCApi["lists"]["edit"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.edit.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - apiUtils.lists.get.invalidate({ listId: req.listId }); - if (res.type === "smart") { - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - } - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.edit.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries( + api.lists.get.queryFilter({ listId: req.listId }), + ); + if (res.type === "smart") { + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }), + ); + } + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useMergeLists( - ...opts: Parameters<typeof api.lists.merge.useMutation> + opts?: Parameters<TRPCApi["lists"]["merge"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.merge.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.targetId }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.merge.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.targetId }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useAddBookmarkToList( - ...opts: Parameters<typeof api.lists.addToList.useMutation> + opts?: Parameters<TRPCApi["lists"]["addToList"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.addToList.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - apiUtils.lists.getListsOfBookmark.invalidate({ - bookmarkId: req.bookmarkId, - }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.addToList.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }), + ); + queryClient.invalidateQueries( + api.lists.getListsOfBookmark.queryFilter({ + bookmarkId: req.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useRemoveBookmarkFromList( - ...opts: Parameters<typeof api.lists.removeFromList.useMutation> + opts?: Parameters<TRPCApi["lists"]["removeFromList"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.removeFromList.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - apiUtils.lists.getListsOfBookmark.invalidate({ - bookmarkId: req.bookmarkId, - }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.removeFromList.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }), + ); + queryClient.invalidateQueries( + api.lists.getListsOfBookmark.queryFilter({ + bookmarkId: req.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteBookmarkList( - ...opts: Parameters<typeof api.lists.delete.useMutation> + opts?: Parameters<TRPCApi["lists"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - apiUtils.lists.get.invalidate({ listId: req.listId }); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries( + api.lists.get.queryFilter({ listId: req.listId }), + ); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useBookmarkLists( - ...opts: Parameters<typeof api.lists.list.useQuery> + input?: Parameters<TRPCApi["lists"]["list"]["queryOptions"]>[0], + opts?: Parameters<TRPCApi["lists"]["list"]["queryOptions"]>[1], ) { - return api.lists.list.useQuery(opts[0], { - ...opts[1], - select: (data) => { - return { data: data.lists, ...listsToTree(data.lists) }; - }, - }); + const api = useTRPC(); + return useQuery( + api.lists.list.queryOptions(input, { + ...opts, + select: (data) => { + return { data: data.lists, ...listsToTree(data.lists) }; + }, + }), + ); } export function augmentBookmarkListsWithInitialData( diff --git a/packages/shared-react/hooks/reader-settings.tsx b/packages/shared-react/hooks/reader-settings.tsx index 2705a050..7258ebe9 100644 --- a/packages/shared-react/hooks/reader-settings.tsx +++ b/packages/shared-react/hooks/reader-settings.tsx @@ -9,6 +9,7 @@ import { useMemo, useState, } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { READER_DEFAULTS, @@ -16,7 +17,7 @@ import { ReaderSettingsPartial, } from "@karakeep/shared/types/readers"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; export interface UseReaderSettingsOptions { /** @@ -39,6 +40,7 @@ export interface UseReaderSettingsOptions { } export function useReaderSettings(options: UseReaderSettingsOptions) { + const api = useTRPC(); const { getLocalOverrides, saveLocalOverrides, @@ -52,8 +54,8 @@ export function useReaderSettings(options: UseReaderSettingsOptions) { const [pendingServerSave, setPendingServerSave] = useState<ReaderSettings | null>(null); - const { data: serverSettings } = api.users.settings.useQuery(); - const apiUtils = api.useUtils(); + const { data: serverSettings } = useQuery(api.users.settings.queryOptions()); + const queryClient = useQueryClient(); // Load local overrides on mount useEffect(() => { @@ -76,30 +78,33 @@ export function useReaderSettings(options: UseReaderSettingsOptions) { } }, [serverSettings, pendingServerSave]); - const { mutate: updateServerSettings, isPending: isSaving } = - api.users.updateSettings.useMutation({ + const { mutate: updateServerSettings, isPending: isSaving } = useMutation( + api.users.updateSettings.mutationOptions({ onSettled: async () => { - await apiUtils.users.settings.refetch(); + await queryClient.refetchQueries(api.users.settings.pathFilter()); }, - }); + }), + ); // Separate mutation for saving defaults (clears local overrides on success) const { mutate: saveServerSettings, isPending: isSavingDefaults } = - api.users.updateSettings.useMutation({ - onSuccess: () => { - // Clear local and session overrides after successful server save - setLocalOverrides({}); - saveLocalOverrides({}); - onClearSessionOverrides?.(); - }, - onError: () => { - // Clear pending state so we don't show values that failed to persist - setPendingServerSave(null); - }, - onSettled: async () => { - await apiUtils.users.settings.refetch(); - }, - }); + useMutation( + api.users.updateSettings.mutationOptions({ + onSuccess: () => { + // Clear local and session overrides after successful server save + setLocalOverrides({}); + saveLocalOverrides({}); + onClearSessionOverrides?.(); + }, + onError: () => { + // Clear pending state so we don't show values that failed to persist + setPendingServerSave(null); + }, + onSettled: async () => { + await queryClient.refetchQueries(api.users.settings.pathFilter()); + }, + }), + ); // Compute effective settings with precedence: session → local → pendingSave → server → default const settings: ReaderSettings = useMemo( diff --git a/packages/shared-react/hooks/rules.ts b/packages/shared-react/hooks/rules.ts index 8428f883..8bca9d69 100644 --- a/packages/shared-react/hooks/rules.ts +++ b/packages/shared-react/hooks/rules.ts @@ -1,40 +1,53 @@ -import { api } from "../trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useCreateRule( - ...opts: Parameters<typeof api.rules.create.useMutation> + opts?: Parameters<TRPCApi["rules"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.rules.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.rules.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.rules.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.rules.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateRule( - ...opts: Parameters<typeof api.rules.update.useMutation> + opts?: Parameters<TRPCApi["rules"]["update"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.rules.update.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.rules.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.rules.update.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.rules.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteRule( - ...opts: Parameters<typeof api.rules.delete.useMutation> + opts?: Parameters<TRPCApi["rules"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.rules.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.rules.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.rules.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.rules.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/tags.ts b/packages/shared-react/hooks/tags.ts index e1e7416f..a616b88a 100644 --- a/packages/shared-react/hooks/tags.ts +++ b/packages/shared-react/hooks/tags.ts @@ -1,120 +1,155 @@ -import { keepPreviousData } from "@tanstack/react-query"; +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import { ZTagListResponse } from "@karakeep/shared/types/tags"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function usePaginatedSearchTags( - input: Parameters<typeof api.tags.list.useInfiniteQuery>[0], + input: Parameters<TRPCApi["tags"]["list"]["infiniteQueryOptions"]>[0], ) { - return api.tags.list.useInfiniteQuery(input, { - placeholderData: keepPreviousData, - getNextPageParam: (lastPage) => lastPage.nextCursor, + const api = useTRPC(); + return useInfiniteQuery({ + ...api.tags.list.infiniteQueryOptions(input, { + placeholderData: keepPreviousData, + getNextPageParam: (lastPage) => lastPage.nextCursor, + gcTime: 60_000, + }), select: (data) => ({ tags: data.pages.flatMap((page) => page.tags), }), - gcTime: 60_000, }); } -export function useTagAutocomplete<T>(opts: { +export function useTagAutocomplete<T = ZTagListResponse>(opts: { nameContains: string; - select?: (tags: ZTagListResponse) => T; + select?: (data: ZTagListResponse) => T; enabled?: boolean; }) { - return api.tags.list.useQuery( - { - nameContains: opts.nameContains, - limit: 50, - sortBy: opts.nameContains ? "relevance" : "usage", - }, - { - select: opts.select, - placeholderData: keepPreviousData, - gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000, - enabled: opts.enabled, - }, - ); + const api = useTRPC(); + return useQuery({ + ...api.tags.list.queryOptions( + { + nameContains: opts.nameContains, + limit: 50, + sortBy: opts.nameContains ? "relevance" : "usage", + }, + { + placeholderData: keepPreviousData, + gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000, + enabled: opts.enabled, + }, + ), + select: opts.select, + }); } export function useCreateTag( - ...opts: Parameters<typeof api.tags.create.useMutation> + opts?: Parameters<TRPCApi["tags"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - - return api.tags.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.tags.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateTag( - ...opts: Parameters<typeof api.tags.update.useMutation> + opts?: Parameters<TRPCApi["tags"]["update"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - - return api.tags.update.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - apiUtils.tags.get.invalidate({ tagId: res.id }); - apiUtils.bookmarks.getBookmarks.invalidate({ tagId: res.id }); - - // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks - apiUtils.bookmarks.getBookmark.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.tags.update.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + queryClient.invalidateQueries( + api.tags.get.queryFilter({ tagId: res.id }), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ tagId: res.id }), + ); + + // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useMergeTag( - ...opts: Parameters<typeof api.tags.merge.useMutation> + opts?: Parameters<TRPCApi["tags"]["merge"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - - return api.tags.merge.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => { - apiUtils.tags.get.invalidate({ tagId }); - apiUtils.bookmarks.getBookmarks.invalidate({ tagId }); - }); - // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks - apiUtils.bookmarks.getBookmark.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.tags.merge.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => { + queryClient.invalidateQueries(api.tags.get.queryFilter({ tagId })); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ tagId }), + ); + }); + // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteTag( - ...opts: Parameters<typeof api.tags.delete.useMutation> + opts?: Parameters<TRPCApi["tags"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - - return api.tags.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.tags.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteUnusedTags( - ...opts: Parameters<typeof api.tags.deleteUnused.useMutation> + opts?: Parameters<TRPCApi["tags"]["deleteUnused"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - - return api.tags.deleteUnused.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.tags.deleteUnused.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/users.ts b/packages/shared-react/hooks/users.ts index b1909761..221681a4 100644 --- a/packages/shared-react/hooks/users.ts +++ b/packages/shared-react/hooks/users.ts @@ -1,37 +1,49 @@ -import { api } from "../trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useUpdateUserSettings( - ...opts: Parameters<typeof api.users.updateSettings.useMutation> + opts?: Parameters<TRPCApi["users"]["updateSettings"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.users.updateSettings.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.users.settings.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.users.updateSettings.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.users.settings.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateUserAvatar( - ...opts: Parameters<typeof api.users.updateAvatar.useMutation> + opts?: Parameters<TRPCApi["users"]["updateAvatar"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.users.updateAvatar.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.users.whoami.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.users.updateAvatar.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.users.whoami.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteAccount( - ...opts: Parameters<typeof api.users.deleteAccount.useMutation> + opts?: Parameters<TRPCApi["users"]["deleteAccount"]["mutationOptions"]>[0], ) { - return api.users.deleteAccount.useMutation(opts[0]); + const api = useTRPC(); + return useMutation(api.users.deleteAccount.mutationOptions(opts)); } export function useWhoAmI() { - return api.users.whoami.useQuery(); + const api = useTRPC(); + return useQuery(api.users.whoami.queryOptions()); } diff --git a/packages/shared-react/package.json b/packages/shared-react/package.json index cf0539be..ce29a24e 100644 --- a/packages/shared-react/package.json +++ b/packages/shared-react/package.json @@ -8,7 +8,8 @@ "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", "@tanstack/react-query": "5.90.2", - "@trpc/client": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/tanstack-react-query": "^11.9.0", "superjson": "^2.2.1" }, "devDependencies": { diff --git a/packages/shared-react/providers/trpc-provider.tsx b/packages/shared-react/providers/trpc-provider.tsx index 696bf195..2c41aa11 100644 --- a/packages/shared-react/providers/trpc-provider.tsx +++ b/packages/shared-react/providers/trpc-provider.tsx @@ -1,9 +1,11 @@ import { useMemo } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { httpBatchLink } from "@trpc/client"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; import superjson from "superjson"; -import { api } from "../trpc"; +import type { AppRouter } from "@karakeep/trpc/routers/_app"; + +import { TRPCProvider } from "../trpc"; interface Settings { apiKey?: string; @@ -12,7 +14,7 @@ interface Settings { } function getTRPCClient(settings: Settings) { - return api.createClient({ + return createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: `${settings.address}/api/trpc`, @@ -31,7 +33,7 @@ function getTRPCClient(settings: Settings) { }); } -export function TRPCProvider({ +export function TRPCSettingsProvider({ settings, children, }: { @@ -42,8 +44,10 @@ export function TRPCProvider({ const trpcClient = useMemo(() => getTRPCClient(settings), [settings]); return ( - <api.Provider client={trpcClient} queryClient={queryClient}> - <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> - </api.Provider> + <QueryClientProvider client={queryClient}> + <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}> + {children} + </TRPCProvider> + </QueryClientProvider> ); } diff --git a/packages/shared-react/trpc.ts b/packages/shared-react/trpc.ts index 1478684f..2a36b257 100644 --- a/packages/shared-react/trpc.ts +++ b/packages/shared-react/trpc.ts @@ -1,7 +1,7 @@ "use client"; -import { createTRPCReact } from "@trpc/react-query"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; import type { AppRouter } from "@karakeep/trpc/routers/_app"; -export const api = createTRPCReact<AppRouter>(); +export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>(); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index d9fa12c0..d50a2174 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -17,7 +17,7 @@ "@karakeep/plugins": "workspace:*", "@karakeep/shared": "workspace:*", "@karakeep/shared-server": "workspace:*", - "@trpc/server": "^11.4.3", + "@trpc/server": "^11.9.0", "bcryptjs": "^2.4.3", "deep-equal": "^2.2.3", "drizzle-orm": "^0.44.2", |
