diff --git a/apps/worxpace/package.json b/apps/worxpace/package.json index 9cc3b5ec..932fafe2 100644 --- a/apps/worxpace/package.json +++ b/apps/worxpace/package.json @@ -16,6 +16,7 @@ "dependencies": { "@acme/i18n": "workspace:*", "@acme/prisma": "workspace:*", + "@acme/trpc": "workspace:*", "@acme/ui": "workspace:*", "@acme/validators": "workspace:*", "@blocknote/core": "^0.15.0", @@ -30,6 +31,7 @@ "@liveblocks/react": "^1.12.0", "@liveblocks/yjs": "^1.12.0", "@t3-oss/env-nextjs": "^0.10.1", + "@trpc/server": "next", "lucide-react": "^0.408.0", "next": "^14.2.3", "react": "18.3.1", diff --git a/apps/worxpace/src/app/api/trpc/[trpc]/route.ts b/apps/worxpace/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 00000000..abb1afb0 --- /dev/null +++ b/apps/worxpace/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,52 @@ +import { appRouter, createTRPCContext } from "@acme/trpc"; +import * as trpcNext from '@trpc/server/adapters/next' + +export const runtime = "edge"; + +/** + * Configure basic CORS headers + * You should extend this to match your needs + */ +const setCorsHeaders = (res: Response) => { + res.headers.set("Access-Control-Allow-Origin", "*"); + res.headers.set("Access-Control-Request-Method", "*"); + res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST"); + res.headers.set("Access-Control-Allow-Headers", "*"); +}; + +export const OPTIONS = () => { + const response = new Response(null, { + status: 204, + }); + setCorsHeaders(response); + return response; +}; + +const handler = trpcNext.createNextApiHandler( { + router: appRouter, + createContext: createTRPCContext, + onError({ error, path }) { + console.error(`>>> tRPC Error on '${path}'`, error); + }, +}) + +// const handler = auth(async (req) => { +// const response = await fetchRequestHandler({ +// endpoint: "/api/trpc", +// router: appRouter, +// req, +// createContext: () => +// createTRPCContext({ +// session: req.auth, +// headers: req.headers, +// }), +// onError({ error, path }) { +// console.error(`>>> tRPC Error on '${path}'`, error); +// }, +// }); + +// setCorsHeaders(response); +// return response; +// }); + +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/apps/worxpace/src/hooks/use-documents.ts b/apps/worxpace/src/hooks/use-documents.ts index eab4f3a0..47710870 100644 --- a/apps/worxpace/src/hooks/use-documents.ts +++ b/apps/worxpace/src/hooks/use-documents.ts @@ -24,7 +24,7 @@ import { const fetcher = async ({ workspaceId }: DocumentsKey) => { try { return await fetchUrl( - `/api/documents?workspaceId=${workspaceId}`, + `/api/trpc/documents?workspaceId=${workspaceId}`, ); } catch (error) { console.log(error); diff --git a/packages/trpc/package.json b/packages/trpc/package.json new file mode 100644 index 00000000..847afba3 --- /dev/null +++ b/packages/trpc/package.json @@ -0,0 +1,41 @@ +{ + "name": "@acme/trpc", + "version": "1.0.3", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint .", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@acme/prisma": "workspace:*", + "@acme/validators": "workspace:*", + "@clerk/nextjs": "^5.2.12", + "@t3-oss/env-nextjs": "^0.10.1", + "@trpc/client": "next", + "@trpc/server": "next", + "superjson": "2.2.1", + "zod": "^3.23.7" + }, + "devDependencies": { + "@acme/eslint-config": "workspace:*", + "@acme/prettier-config": "workspace:*", + "@acme/tsconfig": "workspace:*", + "eslint": "^8.56.0", + "prettier": "^3.2.5", + "typescript": "^5.5.4" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@acme/eslint-config/base" + ] + }, + "prettier": "@acme/prettier-config" +} diff --git a/packages/trpc/src/index.ts b/packages/trpc/src/index.ts new file mode 100644 index 00000000..1cbe6fdd --- /dev/null +++ b/packages/trpc/src/index.ts @@ -0,0 +1,33 @@ +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; + +import type { AppRouter } from "./root"; +import { appRouter } from "./root"; +import { createCallerFactory, createTRPCContext } from "./trpc"; + +/** + * Create a server-side caller for the tRPC API + * @example + * const trpc = createCaller(createContext); + * const res = await trpc.post.all(); + * ^? Post[] + */ +const createCaller = createCallerFactory(appRouter); + +/** + * Inference helpers for input types + * @example + * type PostByIdInput = RouterInputs['post']['byId'] + * ^? { id: number } + **/ +type RouterInputs = inferRouterInputs; + +/** + * Inference helpers for output types + * @example + * type AllPostsOutput = RouterOutputs['post']['all'] + * ^? Post[] + **/ +type RouterOutputs = inferRouterOutputs; + +export { createTRPCContext, appRouter, createCaller }; +export type { AppRouter, RouterInputs, RouterOutputs }; diff --git a/packages/trpc/src/root.ts b/packages/trpc/src/root.ts new file mode 100644 index 00000000..985d6db5 --- /dev/null +++ b/packages/trpc/src/root.ts @@ -0,0 +1,11 @@ +import { authRouter } from "./router/auth"; +import { documentRouter } from "./router/document"; +import { createTRPCRouter } from "./trpc"; + +export const appRouter = createTRPCRouter({ + auth: authRouter, + document: documentRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; diff --git a/packages/trpc/src/router/auth.ts b/packages/trpc/src/router/auth.ts new file mode 100644 index 00000000..a626c1d1 --- /dev/null +++ b/packages/trpc/src/router/auth.ts @@ -0,0 +1,16 @@ +import {currentUser} from "@clerk/nextjs/server" + +import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; + +export const authRouter = createTRPCRouter({ + getAuth: publicProcedure.query(({ ctx }) => { + return ctx.auth; + }), + getUser: publicProcedure.query(() => { + return currentUser() + }), + getSecretMessage: protectedProcedure.query(() => { + // testing type validation of overridden next-auth Session in @acme/auth package + return "you can see this secret message!"; + }), +}); diff --git a/packages/trpc/src/router/document.ts b/packages/trpc/src/router/document.ts new file mode 100644 index 00000000..a3dc5cc9 --- /dev/null +++ b/packages/trpc/src/router/document.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + + +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +const include = { workspace: true, createdBy: true, updatedBy: true } + +export const documentRouter = createTRPCRouter({ + all: protectedProcedure + .input(z.object({ workspaceId: z.string(), isArchived: z.boolean().optional() })) + .query(({ ctx, input }) => { + return ctx.db.document.findMany({ + where: input, + orderBy: { createdAt: "desc" }, + include, + }) + }), + + byId: protectedProcedure + .input(z.string()) + .query(({ ctx, input }) => { + return ctx.db.document.findUnique({ where: { id: input }, include }); + }), +}); diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts new file mode 100644 index 00000000..f1cd00c3 --- /dev/null +++ b/packages/trpc/src/trpc.ts @@ -0,0 +1,104 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1) + * 2. You want to create a new middleware or type of procedure (see Part 3) + * + * tl;dr - this is where all the tRPC server stuff is created and plugged in. + * The pieces you will need to use are documented accordingly near the end + */ +import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { getAuth } from "@clerk/nextjs/server"; +import {worxpace} from "@acme/prisma" +// import { NextApiRequest } from "@trpc/server/adapters/next"; +import * as trpcNext from '@trpc/server/adapters/next' + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: trpcNext.CreateNextContextOptions) => { + const auth = getAuth(opts.req) + // const source = opts.headers.get("x-trpc-source") ?? "unknown"; + // console.log(">>> tRPC Request from", source, "by", auth?.userId); + + return { + auth, + db: worxpace, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the trpc api is initialized, connecting the context and + * transformer + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter: ({ shape, error }) => ({ + ...shape, + data: { + ...shape.data, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }), +}); + +/** + * Create a server-side caller + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these + * a lot in the /src/server/api/routers folder + */ + +/** + * This is how you create new routers and subrouters in your tRPC API + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthed) procedure + * + * This is the base piece you use to build new queries and mutations on your + * tRPC API. It does not guarantee that a user querying is authorized, but you + * can still access user session data if they are logged in + */ +export const publicProcedure = t.procedure; + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the `AuthObject` is valid and guarantees `ctx.auth.userId` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(({ ctx, next }) => { + if (!ctx.auth.userId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `auth` as non-nullable + auth: ctx.auth, + }, + }); +}); diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json new file mode 100644 index 00000000..ba556ecc --- /dev/null +++ b/packages/trpc/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@acme/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 511cc830..38dab80a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,9 @@ importers: '@acme/prisma': specifier: workspace:* version: link:../../packages/prisma + '@acme/trpc': + specifier: workspace:* + version: link:../../packages/trpc '@acme/ui': specifier: workspace:* version: link:../../packages/ui @@ -205,6 +208,9 @@ importers: '@t3-oss/env-nextjs': specifier: ^0.10.1 version: 0.10.1(typescript@5.5.4)(zod@3.23.8) + '@trpc/server': + specifier: next + version: 11.0.0-rc.477 lucide-react: specifier: ^0.408.0 version: 0.408.0(react@18.3.1) @@ -461,6 +467,52 @@ importers: specifier: ^5.5.4 version: 5.5.4 + packages/trpc: + dependencies: + '@acme/prisma': + specifier: workspace:* + version: link:../prisma + '@acme/validators': + specifier: workspace:* + version: link:../validators + '@clerk/nextjs': + specifier: ^5.2.12 + version: 5.2.12(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@t3-oss/env-nextjs': + specifier: ^0.10.1 + version: 0.10.1(typescript@5.5.4)(zod@3.23.8) + '@trpc/client': + specifier: next + version: 11.0.0-rc.477(@trpc/server@11.0.0-rc.477) + '@trpc/server': + specifier: next + version: 11.0.0-rc.477 + superjson: + specifier: 2.2.1 + version: 2.2.1 + zod: + specifier: ^3.23.7 + version: 3.23.8 + devDependencies: + '@acme/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@acme/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@acme/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + eslint: + specifier: ^8.56.0 + version: 8.57.0 + prettier: + specifier: ^3.2.5 + version: 3.3.3 + typescript: + specifier: ^5.5.4 + version: 5.5.4 + packages/ui: dependencies: '@acme/i18n':