diff --git a/src/orpc/client.ts b/src/orpc/client.ts index c631068..0f76faa 100644 --- a/src/orpc/client.ts +++ b/src/orpc/client.ts @@ -22,6 +22,6 @@ const getORPCClient = createIsomorphicFn() return createORPCClient(link) }) -export const client: RouterClient = getORPCClient() +const client: RouterClient = getORPCClient() export const orpc = createTanstackQueryUtils(client) diff --git a/src/orpc/handlers/todo.ts b/src/orpc/handlers/todo.ts new file mode 100644 index 0000000..2f7a92d --- /dev/null +++ b/src/orpc/handlers/todo.ts @@ -0,0 +1,83 @@ +import { ORPCError, os } from '@orpc/server' +import { eq } from 'drizzle-orm' +import { + createInsertSchema, + createSelectSchema, + createUpdateSchema, +} from 'drizzle-zod' +import { z } from 'zod' +import { db } from '@/db' +import { todoTable } from '@/db/schema' + +const selectSchema = createSelectSchema(todoTable) + +const insertSchema = createInsertSchema(todoTable).omit({ + id: true, + createdAt: true, + updatedAt: true, +}) + +const updateSchema = createUpdateSchema(todoTable).omit({ + id: true, + createdAt: true, + updatedAt: true, +}) + +export const list = os + .input(z.void()) + .output(z.array(selectSchema)) + .handler(async () => { + const todos = await db.query.todoTable.findMany({ + orderBy: (todos, { desc }) => [desc(todos.createdAt)], + }) + return todos + }) + +export const create = os + .input(insertSchema) + .output(selectSchema) + .handler(async ({ input }) => { + const [newTodo] = await db + .insert(todoTable) + .values({ title: input.title }) + .returning() + + if (!newTodo) { + throw new ORPCError('NOT_FOUND') + } + + return newTodo + }) + +export const update = os + .input( + z.object({ + id: z.uuid(), + data: updateSchema, + }), + ) + .output(selectSchema) + .handler(async ({ input }) => { + const [updatedTodo] = await db + .update(todoTable) + .set(input.data) + .where(eq(todoTable.id, input.id)) + .returning() + + if (!updatedTodo) { + throw new ORPCError('NOT_FOUND') + } + + return updatedTodo + }) + +export const remove = os + .input( + z.object({ + id: z.uuid(), + }), + ) + .output(z.void()) + .handler(async ({ input }) => { + await db.delete(todoTable).where(eq(todoTable.id, input.id)) + }) diff --git a/src/orpc/index.ts b/src/orpc/index.ts new file mode 100644 index 0000000..55bd9a5 --- /dev/null +++ b/src/orpc/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './router' diff --git a/src/orpc/router.ts b/src/orpc/router.ts new file mode 100644 index 0000000..70b205b --- /dev/null +++ b/src/orpc/router.ts @@ -0,0 +1,5 @@ +import * as todo from './handlers/todo' + +export const router = { + todo, +} diff --git a/src/orpc/router/index.ts b/src/orpc/router/index.ts deleted file mode 100644 index 5a0fdb0..0000000 --- a/src/orpc/router/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { addTodo, listTodos } from './todos' - -export const router = { - listTodos, - addTodo, -} diff --git a/src/orpc/router/todos.ts b/src/orpc/router/todos.ts deleted file mode 100644 index c59afc7..0000000 --- a/src/orpc/router/todos.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { os } from '@orpc/server' -import { z } from 'zod' - -const todos = [ - { id: 1, name: 'Get groceries' }, - { id: 2, name: 'Buy a new phone' }, - { id: 3, name: 'Finish the project' }, -] - -export const listTodos = os.input(z.object({})).handler(() => { - return todos -}) - -export const addTodo = os - .input(z.object({ name: z.string() })) - .handler(({ input }) => { - const newTodo = { id: todos.length + 1, name: input.name } - todos.push(newTodo) - return newTodo - }) diff --git a/src/orpc/schema.ts b/src/orpc/schema.ts deleted file mode 100644 index 77c11cd..0000000 --- a/src/orpc/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from 'zod' - -export const TodoSchema = z.object({ - id: z.number().int().min(1), - name: z.string(), -}) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 03a4520..5d5b392 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -9,13 +9,13 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as TodoRouteImport } from './routes/todo' +import { Route as TodosRouteImport } from './routes/todos' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$' -const TodoRoute = TodoRouteImport.update({ - id: '/todo', - path: '/todo', +const TodosRoute = TodosRouteImport.update({ + id: '/todos', + path: '/todos', getParentRoute: () => rootRouteImport, } as any) const IndexRoute = IndexRouteImport.update({ @@ -31,41 +31,41 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/todo': typeof TodoRoute + '/todos': typeof TodosRoute '/api/rpc/$': typeof ApiRpcSplatRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/todo': typeof TodoRoute + '/todos': typeof TodosRoute '/api/rpc/$': typeof ApiRpcSplatRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute - '/todo': typeof TodoRoute + '/todos': typeof TodosRoute '/api/rpc/$': typeof ApiRpcSplatRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/todo' | '/api/rpc/$' + fullPaths: '/' | '/todos' | '/api/rpc/$' fileRoutesByTo: FileRoutesByTo - to: '/' | '/todo' | '/api/rpc/$' - id: '__root__' | '/' | '/todo' | '/api/rpc/$' + to: '/' | '/todos' | '/api/rpc/$' + id: '__root__' | '/' | '/todos' | '/api/rpc/$' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - TodoRoute: typeof TodoRoute + TodosRoute: typeof TodosRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/todo': { - id: '/todo' - path: '/todo' - fullPath: '/todo' - preLoaderRoute: typeof TodoRouteImport + '/todos': { + id: '/todos' + path: '/todos' + fullPath: '/todos' + preLoaderRoute: typeof TodosRouteImport parentRoute: typeof rootRouteImport } '/': { @@ -87,7 +87,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - TodoRoute: TodoRoute, + TodosRoute: TodosRoute, ApiRpcSplatRoute: ApiRpcSplatRoute, } export const routeTree = rootRouteImport diff --git a/src/routes/todo.tsx b/src/routes/todos.tsx similarity index 78% rename from src/routes/todo.tsx rename to src/routes/todos.tsx index c9b06c3..1dcefac 100644 --- a/src/routes/todo.tsx +++ b/src/routes/todos.tsx @@ -1,97 +1,21 @@ import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' -import { createServerFn } from '@tanstack/react-start' -import { eq } from 'drizzle-orm' import type { ChangeEvent, FormEvent } from 'react' import { useState } from 'react' -import { z } from 'zod' -import { db } from '@/db' -import { todoTable } from '@/db/schema' +import { orpc } from '@/orpc' -const createTodoSchema = z.object({ - title: z.string().min(1, '标题不能为空'), -}) - -const updateTodoSchema = z.object({ - id: z.uuid(), - completed: z.boolean(), -}) - -const deleteTodoSchema = z.object({ - id: z.uuid(), -}) - -const getTodos = createServerFn({ method: 'GET' }).handler(async () => { - const todos = await db.query.todoTable.findMany({ - orderBy: (todos, { desc }) => [desc(todos.createdAt)], - }) - return todos -}) - -const createTodo = createServerFn({ method: 'POST' }) - .inputValidator(createTodoSchema) - .handler(async ({ data }) => { - const [newTodo] = await db - .insert(todoTable) - .values({ title: data.title }) - .returning() - return newTodo - }) - -const updateTodo = createServerFn({ method: 'POST' }) - .inputValidator(updateTodoSchema) - .handler(async ({ data }) => { - const [updatedTodo] = await db - .update(todoTable) - .set({ completed: data.completed }) - .where(eq(todoTable.id, data.id)) - .returning() - return updatedTodo - }) - -const deleteTodo = createServerFn({ method: 'POST' }) - .inputValidator(deleteTodoSchema) - .handler(async ({ data }) => { - await db.delete(todoTable).where(eq(todoTable.id, data.id)) - return { success: true } - }) - -export const Route = createFileRoute('/todo')({ +export const Route = createFileRoute('/todos')({ component: Todo, }) function Todo() { const [newTodoTitle, setNewTodoTitle] = useState('') - const { data: todos, refetch } = useSuspenseQuery({ - queryKey: ['todos'], - queryFn: () => getTodos(), - }) - // Mutations - const createMutation = useMutation({ - mutationFn: (title: string) => createTodo({ data: { title } }), - onSuccess: () => { - setNewTodoTitle('') - refetch() - }, - }) + const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions()) + const createMutation = useMutation(orpc.todo.create.mutationOptions()) + const updateMutation = useMutation(orpc.todo.update.mutationOptions()) + const deleteMutation = useMutation(orpc.todo.remove.mutationOptions()) - const updateMutation = useMutation({ - mutationFn: ({ id, completed }: { id: string; completed: boolean }) => - updateTodo({ data: { id, completed } }), - onSuccess: () => { - refetch() - }, - }) - - const deleteMutation = useMutation({ - mutationFn: (id: string) => deleteTodo({ data: { id } }), - onSuccess: () => { - refetch() - }, - }) - - // Handlers const handleCreateTodo = (e: FormEvent) => { e.preventDefault() if (newTodoTitle.trim()) {