diff options
| author | Fuwn <[email protected]> | 2026-02-03 19:56:19 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-03 19:56:37 -0800 |
| commit | 6be34a75cd060444734080ac14d3a33eea630f15 (patch) | |
| tree | beda873457cf5d6f73e97391c902acf8a9b69712 /packages/web/src/server | |
| parent | feat(iku): Support comment-aware whitespace checking (diff) | |
| download | archived-imemio-6be34a75cd060444734080ac14d3a33eea630f15.tar.xz archived-imemio-6be34a75cd060444734080ac14d3a33eea630f15.zip | |
feat(web): Initialise T3 Stack
Diffstat (limited to 'packages/web/src/server')
| -rw-r--r-- | packages/web/src/server/api/root.ts | 23 | ||||
| -rw-r--r-- | packages/web/src/server/api/routers/post.ts | 38 | ||||
| -rw-r--r-- | packages/web/src/server/api/trpc.ts | 134 | ||||
| -rw-r--r-- | packages/web/src/server/auth/config.ts | 66 | ||||
| -rw-r--r-- | packages/web/src/server/auth/index.ts | 8 | ||||
| -rw-r--r-- | packages/web/src/server/db/index.ts | 17 | ||||
| -rw-r--r-- | packages/web/src/server/db/schema.ts | 100 |
7 files changed, 386 insertions, 0 deletions
diff --git a/packages/web/src/server/api/root.ts b/packages/web/src/server/api/root.ts new file mode 100644 index 0000000..374285c --- /dev/null +++ b/packages/web/src/server/api/root.ts @@ -0,0 +1,23 @@ +import { postRouter } from "~/server/api/routers/post"; +import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +export const appRouter = createTRPCRouter({ + post: postRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; + +/** + * Create a server-side caller for the tRPC API. + * @example + * const trpc = createCaller(createContext); + * const res = await trpc.post.all(); + * ^? Post[] + */ +export const createCaller = createCallerFactory(appRouter); diff --git a/packages/web/src/server/api/routers/post.ts b/packages/web/src/server/api/routers/post.ts new file mode 100644 index 0000000..2bd03a4 --- /dev/null +++ b/packages/web/src/server/api/routers/post.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; +import { posts } from "~/server/db/schema"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + await ctx.db.insert(posts).values({ + name: input.name, + createdById: ctx.session.user.id, + }); + }), + + getLatest: protectedProcedure.query(async ({ ctx }) => { + const post = await ctx.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + + return post ?? null; + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/packages/web/src/server/api/trpc.ts b/packages/web/src/server/api/trpc.ts new file mode 100644 index 0000000..65c86f4 --- /dev/null +++ b/packages/web/src/server/api/trpc.ts @@ -0,0 +1,134 @@ +/** + * 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 { auth } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 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: { headers: Headers }) => { + const session = await auth(); + + return { + db, + session, + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context<typeof createTRPCContext>().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...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" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an artificial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + const end = Date.now(); + + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) 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.use(timingMiddleware); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(({ ctx, next }) => { + if (!ctx.session?.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); + }); diff --git a/packages/web/src/server/auth/config.ts b/packages/web/src/server/auth/config.ts new file mode 100644 index 0000000..b3307cc --- /dev/null +++ b/packages/web/src/server/auth/config.ts @@ -0,0 +1,66 @@ +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import type { DefaultSession, NextAuthConfig } from "next-auth"; +import DiscordProvider from "next-auth/providers/discord"; +import { db } from "~/server/db"; +import { + accounts, + sessions, + users, + verificationTokens, +} from "~/server/db/schema"; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +/** + * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. + * + * @see https://next-auth.js.org/configuration/options + */ +export const authConfig = { + providers: [ + DiscordProvider, + /** + * ...add more providers here. + * + * Most other providers require a bit more work than the Discord provider. For example, the + * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account + * model. Refer to the NextAuth.js docs for the provider you want to use. Example: + * + * @see https://next-auth.js.org/providers/github + */ + ], + adapter: DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, + }), + callbacks: { + session: ({ session, user }) => ({ + ...session, + user: { + ...session.user, + id: user.id, + }, + }), + }, +} satisfies NextAuthConfig; diff --git a/packages/web/src/server/auth/index.ts b/packages/web/src/server/auth/index.ts new file mode 100644 index 0000000..21f0ee0 --- /dev/null +++ b/packages/web/src/server/auth/index.ts @@ -0,0 +1,8 @@ +import NextAuth from "next-auth"; +import { cache } from "react"; +import { authConfig } from "./config"; + +const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig); +const auth = cache(uncachedAuth); + +export { auth, handlers, signIn, signOut }; diff --git a/packages/web/src/server/db/index.ts b/packages/web/src/server/db/index.ts new file mode 100644 index 0000000..f6e5086 --- /dev/null +++ b/packages/web/src/server/db/index.ts @@ -0,0 +1,17 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { env } from "~/env"; +import * as schema from "./schema"; + +/** + * Cache the database connection in development. This avoids creating a new connection on every HMR + * update. + */ +const globalForDb = globalThis as unknown as { + conn: postgres.Sql | undefined; +}; +const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); + +if (env.NODE_ENV !== "production") globalForDb.conn = conn; + +export const db = drizzle(conn, { schema }); diff --git a/packages/web/src/server/db/schema.ts b/packages/web/src/server/db/schema.ts new file mode 100644 index 0000000..08c1551 --- /dev/null +++ b/packages/web/src/server/db/schema.ts @@ -0,0 +1,100 @@ +import { relations } from "drizzle-orm"; +import { index, pgTableCreator, primaryKey } from "drizzle-orm/pg-core"; +import type { AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = pgTableCreator((name) => `web_${name}`); +export const posts = createTable( + "post", + (d) => ({ + id: d.integer().primaryKey().generatedByDefaultAsIdentity(), + name: d.varchar({ length: 256 }), + createdById: d + .varchar({ length: 255 }) + .notNull() + .references(() => users.id), + createdAt: d + .timestamp({ withTimezone: true }) + .$defaultFn(() => /* @__PURE__ */ new Date()) + .notNull(), + updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()), + }), + (t) => [ + index("created_by_idx").on(t.createdById), + index("name_idx").on(t.name), + ], +); +export const users = createTable("user", (d) => ({ + id: d + .varchar({ length: 255 }) + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: d.varchar({ length: 255 }), + email: d.varchar({ length: 255 }).notNull(), + emailVerified: d + .timestamp({ + mode: "date", + withTimezone: true, + }) + .$defaultFn(() => /* @__PURE__ */ new Date()), + image: d.varchar({ length: 255 }), +})); +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), +})); +export const accounts = createTable( + "account", + (d) => ({ + userId: d + .varchar({ length: 255 }) + .notNull() + .references(() => users.id), + type: d.varchar({ length: 255 }).$type<AdapterAccount["type"]>().notNull(), + provider: d.varchar({ length: 255 }).notNull(), + providerAccountId: d.varchar({ length: 255 }).notNull(), + refresh_token: d.text(), + access_token: d.text(), + expires_at: d.integer(), + token_type: d.varchar({ length: 255 }), + scope: d.varchar({ length: 255 }), + id_token: d.text(), + session_state: d.varchar({ length: 255 }), + }), + (t) => [ + primaryKey({ columns: [t.provider, t.providerAccountId] }), + index("account_user_id_idx").on(t.userId), + ], +); +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); +export const sessions = createTable( + "session", + (d) => ({ + sessionToken: d.varchar({ length: 255 }).notNull().primaryKey(), + userId: d + .varchar({ length: 255 }) + .notNull() + .references(() => users.id), + expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(), + }), + (t) => [index("t_user_id_idx").on(t.userId)], +); +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); +export const verificationTokens = createTable( + "verification_token", + (d) => ({ + identifier: d.varchar({ length: 255 }).notNull(), + token: d.varchar({ length: 255 }).notNull(), + expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(), + }), + (t) => [primaryKey({ columns: [t.identifier, t.token] })], +); |