Initial commit

This commit is contained in:
Meier Lukas
2023-12-08 22:35:15 +01:00
commit a2cedf73c0
74 changed files with 15357 additions and 0 deletions

18
packages/api/index.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import type { AppRouter } from "./src/root";
export { appRouter, type AppRouter } from "./src/root";
export { createTRPCContext } from "./src/trpc";
/**
* Inference helpers for input types
* @example type HelloInput = RouterInputs['example']['hello']
**/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helpers for output types
* @example type HelloOutput = RouterOutputs['example']['hello']
**/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

37
packages/api/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "@acme/api",
"version": "0.1.0",
"private": true,
"main": "./index.ts",
"types": "./index.ts",
"license": "MIT",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@acme/auth": "workspace:^0.1.0",
"@acme/db": "workspace:^0.1.0",
"@trpc/client": "next",
"@trpc/server": "next",
"superjson": "2.2.1",
"zod": "^3.22.2"
},
"devDependencies": {
"@acme/eslint-config": "workspace:^0.2.0",
"@acme/prettier-config": "workspace:^0.1.0",
"@acme/tsconfig": "workspace:^0.1.0",
"eslint": "^8.53.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
},
"eslintConfig": {
"root": true,
"extends": [
"@acme/eslint-config/base"
]
},
"prettier": "@acme/prettier-config"
}

11
packages/api/src/root.ts Normal file
View File

@@ -0,0 +1,11 @@
import { authRouter } from "./router/auth";
import { postRouter } from "./router/post";
import { createTRPCRouter } from "./trpc";
export const appRouter = createTRPCRouter({
auth: authRouter,
post: postRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,11 @@
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const authRouter = createTRPCRouter({
getSession: publicProcedure.query(({ ctx }) => {
return ctx.session;
}),
getSecretMessage: protectedProcedure.query(() => {
// testing type validation of overridden next-auth Session in @acme/auth package
return "you can see this secret message!";
}),
});

View File

@@ -0,0 +1,40 @@
import { z } from "zod";
import { desc, eq, schema } from "@acme/db";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const postRouter = createTRPCRouter({
all: publicProcedure.query(({ ctx }) => {
// return ctx.db.select().from(schema.post).orderBy(desc(schema.post.id));
return ctx.db.query.post.findMany({ orderBy: desc(schema.post.id) });
}),
byId: publicProcedure
.input(z.object({ id: z.number() }))
.query(({ ctx, input }) => {
// return ctx.db
// .select()
// .from(schema.post)
// .where(eq(schema.post.id, input.id));
return ctx.db.query.post.findFirst({
where: eq(schema.post.id, input.id),
});
}),
create: protectedProcedure
.input(
z.object({
title: z.string().min(1),
content: z.string().min(1),
}),
)
.mutation(({ ctx, input }) => {
return ctx.db.insert(schema.post).values(input);
}),
delete: protectedProcedure.input(z.number()).mutation(({ ctx, input }) => {
return ctx.db.delete(schema.post).where(eq(schema.post.id, input));
}),
});

132
packages/api/src/trpc.ts Normal file
View File

@@ -0,0 +1,132 @@
/**
* 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 "@acme/auth";
import type { Session } from "@acme/auth";
import { db } from "@acme/db";
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API
*
* These allow you to access things like the database, the session, etc, when
* processing a request
*
*/
interface CreateContextOptions {
session: Session | null;
}
/**
* This helper generates the "internals" for a tRPC context. If you need to use
* it, you can export it from here
*
* Examples of things you may need it for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
db,
};
};
/**
* This is the actual context you'll use in your router. It will be used to
* process every request that goes through your tRPC endpoint
* @link https://trpc.io/docs/context
*/
export const createTRPCContext = async (opts: {
req?: Request;
auth: Session | null;
}) => {
const session = opts.auth ?? (await auth());
const source = opts.req?.headers.get("x-trpc-source") ?? "unknown";
console.log(">>> tRPC Request from", source, "by", session?.user);
return createInnerTRPCContext({
session,
});
};
/**
* 2. INITIALIZATION
*
* This is where the trpc api is initialized, connecting the context and
* transformer
*/
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,
},
};
},
});
/**
* 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;
/**
* Reusable middleware that enforces users are logged in before running the
* procedure
*/
const enforceUserIsAuthed = t.middleware(({ 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 },
},
});
});
/**
* Protected (authed) 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(enforceUserIsAuthed);

View File

@@ -0,0 +1,8 @@
{
"extends": "@acme/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}

28
packages/auth/env.mjs Normal file
View File

@@ -0,0 +1,28 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
AUTH_DISCORD_ID: z.string().min(1),
AUTH_DISCORD_SECRET: z.string().min(1),
AUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
AUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string() : z.string().url(),
),
},
client: {},
runtimeEnv: {
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
AUTH_SECRET: process.env.AUTH_SECRET,
AUTH_URL: process.env.AUTH_URL,
},
skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION,
});

38
packages/auth/index.ts Normal file
View File

@@ -0,0 +1,38 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* @see https://github.com/nextauthjs/next-auth/pull/8932 */
import Discord from "@auth/core/providers/discord";
import type { DefaultSession } from "@auth/core/types";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import NextAuth from "next-auth";
import { db, tableCreator } from "@acme/db";
export type { Session } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
adapter: DrizzleAdapter(db, tableCreator),
providers: [Discord],
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
},
}),
},
});

View File

@@ -0,0 +1,40 @@
{
"name": "@acme/auth",
"version": "0.1.0",
"private": true,
"main": "./index.ts",
"types": "./index.ts",
"license": "MIT",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@acme/db": "workspace:^0.1.0",
"@auth/core": "^0.18.4",
"@auth/drizzle-adapter": "^0.3.9",
"@t3-oss/env-nextjs": "^0.7.1",
"next": "^14.0.3",
"next-auth": "5.0.0-beta.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "^3.22.2"
},
"devDependencies": {
"@acme/eslint-config": "workspace:^0.2.0",
"@acme/prettier-config": "workspace:^0.1.0",
"@acme/tsconfig": "workspace:^0.1.0",
"eslint": "^8.53.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
},
"eslintConfig": {
"root": true,
"extends": [
"@acme/eslint-config/base"
]
},
"prettier": "@acme/prettier-config"
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@acme/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,23 @@
import * as dotenv from "dotenv";
import type { Config } from "drizzle-kit";
dotenv.config({ path: "../../.env" });
const uri = [
"mysql://",
process.env.DB_USERNAME,
":",
process.env.DB_PASSWORD,
"@",
process.env.DB_HOST,
":3306/",
process.env.DB_NAME,
'?ssl={"rejectUnauthorized":true}',
].join("");
export default {
schema: "./schema",
driver: "mysql2",
dbCredentials: { uri },
tablesFilter: ["t3turbo_*"],
} satisfies Config;

20
packages/db/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import { Client } from "@planetscale/database";
import { drizzle } from "drizzle-orm/planetscale-serverless";
import * as auth from "./schema/auth";
import * as post from "./schema/post";
export const schema = { ...auth, ...post };
export { mySqlTable as tableCreator } from "./schema/_table";
export * from "drizzle-orm";
export const db = drizzle(
new Client({
host: process.env.DB_HOST,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
}).connection(),
{ schema },
);

37
packages/db/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "@acme/db",
"version": "0.1.0",
"private": true,
"main": "./index.ts",
"types": "./index.ts",
"license": "MIT",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"push": "drizzle-kit push:mysql",
"studio": "drizzle-kit studio",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@planetscale/database": "^1.11.0",
"drizzle-orm": "^0.29.1"
},
"devDependencies": {
"@acme/eslint-config": "workspace:^0.2.0",
"@acme/prettier-config": "workspace:^0.1.0",
"@acme/tsconfig": "workspace:^0.1.0",
"dotenv-cli": "^7.3.0",
"drizzle-kit": "^0.20.6",
"eslint": "^8.53.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
},
"eslintConfig": {
"root": true,
"extends": [
"@acme/eslint-config/base"
]
},
"prettier": "@acme/prettier-config"
}

View File

@@ -0,0 +1,9 @@
import { mysqlTableCreator } from "drizzle-orm/mysql-core";
/**
* 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 mySqlTable = mysqlTableCreator((name) => `t3turbo_${name}`);

View File

@@ -0,0 +1,84 @@
import type { AdapterAccount } from "@auth/core/adapters";
import { relations, sql } from "drizzle-orm";
import {
index,
int,
primaryKey,
text,
timestamp,
varchar,
} from "drizzle-orm/mysql-core";
import { mySqlTable } from "./_table";
export const users = mySqlTable("user", {
id: varchar("id", { length: 255 }).notNull().primaryKey(),
name: varchar("name", { length: 255 }),
email: varchar("email", { length: 255 }).notNull(),
emailVerified: timestamp("emailVerified", {
mode: "date",
fsp: 3,
}).default(sql`CURRENT_TIMESTAMP(3)`),
image: varchar("image", { length: 255 }),
});
export const usersRelations = relations(users, ({ many }) => ({
accounts: many(accounts),
}));
export const accounts = mySqlTable(
"account",
{
userId: varchar("userId", { length: 255 }).notNull(),
type: varchar("type", { length: 255 })
.$type<AdapterAccount["type"]>()
.notNull(),
provider: varchar("provider", { length: 255 }).notNull(),
providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(),
refresh_token: varchar("refresh_token", { length: 255 }),
access_token: varchar("access_token", { length: 255 }),
expires_at: int("expires_at"),
token_type: varchar("token_type", { length: 255 }),
scope: varchar("scope", { length: 255 }),
id_token: text("id_token"),
session_state: varchar("session_state", { length: 255 }),
},
(account) => ({
compoundKey: primaryKey(account.provider, account.providerAccountId),
userIdIdx: index("userId_idx").on(account.userId),
}),
);
export const accountsRelations = relations(accounts, ({ one }) => ({
user: one(users, { fields: [accounts.userId], references: [users.id] }),
}));
export const sessions = mySqlTable(
"session",
{
sessionToken: varchar("sessionToken", { length: 255 })
.notNull()
.primaryKey(),
userId: varchar("userId", { length: 255 }).notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(session) => ({
userIdIdx: index("userId_idx").on(session.userId),
}),
);
export const sessionsRelations = relations(sessions, ({ one }) => ({
user: one(users, { fields: [sessions.userId], references: [users.id] }),
}));
export const verificationTokens = mySqlTable(
"verificationToken",
{
identifier: varchar("identifier", { length: 255 }).notNull(),
token: varchar("token", { length: 255 }).notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey(vt.identifier, vt.token),
}),
);

View File

@@ -0,0 +1,14 @@
import { sql } from "drizzle-orm";
import { serial, timestamp, varchar } from "drizzle-orm/mysql-core";
import { mySqlTable } from "./_table";
export const post = mySqlTable("post", {
id: serial("id").primaryKey(),
title: varchar("name", { length: 256 }).notNull(),
content: varchar("content", { length: 256 }).notNull(),
createdAt: timestamp("created_at")
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updatedAt").onUpdateNow(),
});

View File

@@ -0,0 +1,8 @@
{
"extends": "@acme/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}