This commit is contained in:
Atridad Lahiji 2023-08-12 18:17:36 -06:00 committed by atridadl
parent 24a6aac703
commit 869fdc3bbe
No known key found for this signature in database
12 changed files with 999 additions and 176 deletions

12
drizzle.config.ts Normal file
View file

@ -0,0 +1,12 @@
import type { Config } from "drizzle-kit";
import "dotenv/config";
export default {
schema: "./src/server/schema.ts",
out: "./drizzle/generated",
driver: "mysql2",
breakpoints: true,
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;

View file

@ -15,6 +15,8 @@
"@ably-labs/react-hooks": "^2.1.1",
"@auth/prisma-adapter": "^1.0.1",
"@clerk/nextjs": "^4.23.2",
"@paralleldrive/cuid2": "^2.2.2",
"@planetscale/database": "^1.10.0",
"@prisma/client": "5.1.1",
"@react-email/components": "^0.0.7",
"@tanstack/react-query": "^4.32.6",
@ -27,6 +29,8 @@
"@upstash/redis": "^1.22.0",
"ably": "^1.2.43",
"autoprefixer": "^10.4.14",
"dotenv": "^16.3.1",
"drizzle-orm": "^0.28.2",
"json2csv": "6.0.0-alpha.2",
"next": "^13.4.13",
"nextjs-cors": "^2.1.2",
@ -49,6 +53,7 @@
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"daisyui": "^3.5.1",
"drizzle-kit": "^0.19.12",
"eslint": "^8.46.0",
"eslint-config-next": "^13.4.13",
"prisma": "5.1.1",

796
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -10,9 +10,9 @@ export default createNextApiHandler({
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
);
}
: undefined,
});

View file

@ -165,16 +165,16 @@ const RoomBody = ({}) => {
})
.concat({
id: "LATEST",
createdAt: new Date(),
created_at: new Date(),
userId: roomFromDb.userId,
roomId: roomFromDb.id,
scale: roomScale,
votes: votesFromDb.map((vote) => {
return {
name: vote.userId,
value: vote.value,
};
}),
room: roomFromDb,
roomName: roomFromDb.roomName,
storyName: storyNameText,
});

View file

@ -4,6 +4,8 @@ import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { TRPCError } from "@trpc/server";
import { fetchCache, setCache } from "~/server/redis";
import { sql } from "drizzle-orm";
import { rooms, votes } from "~/server/schema";
export const restRouter = createTRPCRouter({
dbWarmer: publicProcedure
@ -13,7 +15,7 @@ export const restRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
const isValidKey = await validateApiKey(input.key);
if (isValidKey) {
await ctx.prisma.vote.findMany();
await ctx.db.query.votes.findMany();
return "Toasted the DB";
} else {
throw new TRPCError({ code: "UNAUTHORIZED" });
@ -30,7 +32,11 @@ export const restRouter = createTRPCRouter({
if (cachedResult) {
return cachedResult;
} else {
const votesCount = await ctx.prisma.vote.count();
const votesResult = (
await ctx.db.select({ count: sql<number>`count(*)` }).from(votes)
)[0];
const votesCount = votesResult ? Number(votesResult.count) : 0;
await setCache(`kv_votecount`, votesCount);
@ -48,7 +54,11 @@ export const restRouter = createTRPCRouter({
if (cachedResult) {
return cachedResult;
} else {
const roomsCount = await ctx.prisma.room.count();
const roomsResult = (
await ctx.db.select({ count: sql<number>`count(*)` }).from(rooms)
)[0];
const roomsCount = roomsResult ? Number(roomsResult.count) : 0;
await setCache(`kv_roomcount`, roomsCount);

View file

@ -3,7 +3,10 @@ import { publishToChannel } from "~/server/ably";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
import { logs, rooms, votes } from "~/server/schema";
import { EventTypes } from "~/utils/types";
import { createId } from "@paralleldrive/cuid2";
import { eq } from "drizzle-orm";
export const roomRouter = createTRPCRouter({
// Create
@ -14,18 +17,18 @@ export const roomRouter = createTRPCRouter({
})
)
.mutation(async ({ ctx, input }) => {
const room = await ctx.prisma.room.create({
data: {
userId: ctx.auth.userId,
roomName: input.name,
storyName: "First Story!",
scale: "0.5,1,2,3,5,8",
visible: false,
},
const room = await ctx.db.insert(rooms).values({
id: createId(),
userId: ctx.auth.userId,
roomName: input.name,
storyName: "First Story!",
scale: "0.5,1,2,3,5,8",
visible: false,
});
const success = room.rowsAffected > 0;
if (room) {
await invalidateCache(`kv_roomcount`);
console.log("PUBLISHED TO ", `kv_roomlist_${ctx.auth.userId}`);
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
await publishToChannel(
@ -40,26 +43,26 @@ export const roomRouter = createTRPCRouter({
JSON.stringify(room)
);
}
// happy path
return !!room;
return success;
}),
// Get One
get: protectedProcedure
.input(z.object({ id: z.string() }))
.query(({ ctx, input }) => {
return ctx.prisma.room.findUnique({
where: {
id: input.id,
},
select: {
id: true,
userId: true,
logs: true,
roomName: true,
storyName: true,
visible: true,
scale: true,
return ctx.db.query.rooms.findFirst({
where: eq(rooms.id, input.id),
with: {
logs: {
with: {
room: true,
},
},
votes: {
with: {
room: true,
},
},
},
});
}),
@ -77,15 +80,8 @@ export const roomRouter = createTRPCRouter({
if (cachedResult) {
return cachedResult;
} else {
const roomList = await ctx.prisma.room.findMany({
where: {
userId: ctx.auth.userId,
},
select: {
id: true,
createdAt: true,
roomName: true,
},
const roomList = await ctx.db.query.rooms.findMany({
where: eq(rooms.userId, ctx.auth.userId),
});
await setCache(`kv_roomlist_${ctx.auth.userId}`, roomList);
@ -109,98 +105,76 @@ export const roomRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
if (input.reset) {
if (input.log) {
const oldRoom = await ctx.prisma.room.findUnique({
where: {
id: input.roomId,
},
select: {
roomName: true,
storyName: true,
scale: true,
votes: {
select: {
userId: true,
value: true,
},
},
const oldRoom = await ctx.db.query.rooms.findFirst({
where: eq(rooms.id, input.roomId),
with: {
votes: true,
logs: true,
},
});
oldRoom &&
(await ctx.prisma.log.create({
data: {
userId: ctx.auth.userId,
roomId: input.roomId,
scale: oldRoom.scale,
votes: oldRoom.votes.map((vote) => {
return {
name: vote.userId,
value: vote.value,
};
}),
roomName: oldRoom.roomName,
storyName: oldRoom.storyName,
},
(await ctx.db.insert(logs).values({
id: createId(),
userId: ctx.auth.userId,
roomId: input.roomId,
scale: oldRoom.scale,
votes: oldRoom.votes.map((vote) => {
return {
name: vote.userId,
value: vote.value,
};
}),
roomName: oldRoom.roomName,
storyName: oldRoom.storyName,
}));
}
await ctx.prisma.vote.deleteMany({
where: {
roomId: input.roomId,
},
});
await ctx.db.delete(votes).where(eq(votes.roomId, input.roomId));
await invalidateCache(`kv_votes_${input.roomId}`);
}
const newRoom = await ctx.prisma.room.update({
where: {
id: input.roomId,
},
data: {
const newRoom = await ctx.db
.update(rooms)
.set({
storyName: input.name,
userId: ctx.auth.userId,
visible: input.visible,
scale: [...new Set(input.scale.split(","))]
.filter((item) => item !== "")
.toString(),
},
select: {
id: true,
roomName: true,
storyName: true,
visible: true,
scale: true,
votes: {
select: {
value: true,
},
},
},
});
})
.where(eq(rooms.id, input.roomId));
if (newRoom) {
const success = newRoom.rowsAffected > 0;
if (success) {
await publishToChannel(
`${newRoom.id}`,
`${input.roomId}`,
EventTypes.ROOM_UPDATE,
JSON.stringify(newRoom)
);
}
return !!newRoom;
return success;
}),
// Delete One
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const deletedRoom = await ctx.prisma.room.delete({
where: {
id: input.id,
},
});
const deletedRoom = await ctx.db
.delete(rooms)
.where(eq(rooms.id, input.id));
const success = deletedRoom.rowsAffected > 0;
if (success) {
await ctx.db.delete(votes).where(eq(votes.roomId, input.id));
await ctx.db.delete(logs).where(eq(logs.roomId, input.id));
if (deletedRoom) {
await invalidateCache(`kv_roomcount`);
await invalidateCache(`kv_votecount`);
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
@ -212,7 +186,7 @@ export const roomRouter = createTRPCRouter({
);
await publishToChannel(
`${deletedRoom.id}`,
`${input.id}`,
EventTypes.ROOM_UPDATE,
JSON.stringify(deletedRoom)
);
@ -224,6 +198,6 @@ export const roomRouter = createTRPCRouter({
);
}
return !!deletedRoom;
return success;
}),
});

View file

@ -1,10 +1,12 @@
import { z } from "zod";
import { publishToChannel } from "~/server/ably";
import type { Room } from "@prisma/client";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
import { EventTypes } from "~/utils/types";
import { eq } from "drizzle-orm";
import { votes } from "~/server/schema";
import { createId } from "@paralleldrive/cuid2";
export const voteRouter = createTRPCRouter({
getAllByRoomId: protectedProcedure
@ -12,10 +14,9 @@ export const voteRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
const cachedResult = await fetchCache<
{
value: string;
room: Room;
id: string;
createdAt: Date;
value: string;
created_at: Date;
userId: string;
roomId: string;
}[]
@ -24,18 +25,8 @@ export const voteRouter = createTRPCRouter({
if (cachedResult) {
return cachedResult;
} else {
const votesByRoomId = await ctx.prisma.vote.findMany({
where: {
roomId: input.roomId,
},
select: {
id: true,
createdAt: true,
room: true,
roomId: true,
userId: true,
value: true,
},
const votesByRoomId = await ctx.db.query.votes.findMany({
where: eq(votes.roomId, input.roomId),
});
await setCache(`kv_votes_${input.roomId}`, votesByRoomId);
@ -46,37 +37,30 @@ export const voteRouter = createTRPCRouter({
set: protectedProcedure
.input(z.object({ value: z.string(), roomId: z.string() }))
.mutation(async ({ ctx, input }) => {
const vote = await ctx.prisma.vote.upsert({
where: {
userId_roomId: {
roomId: input.roomId,
const vote = await ctx.db
.insert(votes)
.values({
id: createId(),
value: input.value,
userId: ctx.auth.userId,
roomId: input.roomId,
})
.onDuplicateKeyUpdate({
set: {
value: input.value,
userId: ctx.auth.userId,
roomId: input.roomId,
},
},
create: {
value: input.value,
userId: ctx.auth.userId,
roomId: input.roomId,
},
update: {
value: input.value,
userId: ctx.auth.userId,
roomId: input.roomId,
},
select: {
value: true,
userId: true,
roomId: true,
id: true,
},
});
});
if (vote) {
const success = vote.rowsAffected > 0;
if (success) {
await invalidateCache(`kv_votecount`);
await invalidateCache(`kv_votes_${input.roomId}`);
await publishToChannel(
`${vote.roomId}`,
`${input.roomId}`,
EventTypes.VOTE_UPDATE,
input.value
);
@ -84,10 +68,10 @@ export const voteRouter = createTRPCRouter({
await publishToChannel(
`stats`,
EventTypes.STATS_UPDATE,
JSON.stringify(vote)
JSON.stringify(success)
);
}
return !!vote;
return success;
}),
});

View file

@ -23,7 +23,7 @@ import type {
SignedOutAuthObject,
} from "@clerk/nextjs/api";
import { prisma } from "../db";
import { db } from "../db";
interface AuthContext {
auth: SignedInAuthObject | SignedOutAuthObject;
@ -40,7 +40,7 @@ interface AuthContext {
const createInnerTRPCContext = ({ auth }: AuthContext) => {
return {
auth,
prisma,
db,
};
};
@ -61,7 +61,7 @@ export const createTRPCContext = (opts: CreateNextContextOptions) => {
*/
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { OpenApiMeta } from "trpc-openapi";
import type { OpenApiMeta } from "trpc-openapi";
const t = initTRPC
.context<typeof createTRPCContext>()

View file

@ -1,14 +1,11 @@
import { PrismaClient } from "@prisma/client";
import { drizzle } from "drizzle-orm/planetscale-serverless";
import { connect } from "@planetscale/database";
import { env } from "~/env.mjs";
import * as schema from "~/server/schema";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
// create the connection
const connection = connect({
url: env.DATABASE_URL,
});
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export const db = drizzle(connection, { schema });

View file

@ -1,14 +1,10 @@
import { Redis } from "@upstash/redis";
import https from "https";
import { env } from "~/env.mjs";
export const redis = Redis.fromEnv({
agent: new https.Agent({ keepAlive: true }),
});
export const redis = Redis.fromEnv();
export const setCache = async <T>(key: string, value: T) => {
try {
console.log("KEY: ", key);
await redis.set(`${env.APP_ENV}_${key}`, value, {
ex: Number(env.UPSTASH_REDIS_EXPIRY_SECONDS),
});
@ -21,14 +17,8 @@ export const setCache = async <T>(key: string, value: T) => {
export const fetchCache = async <T>(key: string) => {
try {
const result = await redis.get(`${env.APP_ENV}_${key}`);
if (result) {
console.log("CACHE HIT");
} else {
console.log("CACHE MISS");
}
return result as T;
} catch {
console.log("CACHE ERROR");
return null;
}
};

65
src/server/schema.ts Normal file
View file

@ -0,0 +1,65 @@
import {
timestamp,
mysqlTable,
varchar,
boolean,
json,
} from "drizzle-orm/mysql-core";
import { relations } from "drizzle-orm";
export const rooms = mysqlTable("Room", {
id: varchar("id", { length: 255 }).notNull().primaryKey(),
created_at: timestamp("created_at", {
mode: "date",
fsp: 3,
}).defaultNow(),
userId: varchar("userId", { length: 255 }).notNull(),
roomName: varchar("roomName", { length: 255 }),
storyName: varchar("storyName", { length: 255 }),
visible: boolean("visible").default(false).notNull(),
scale: varchar("scale", { length: 255 }).default("0.5,1,2,3,5").notNull(),
});
export const roomsRelations = relations(rooms, ({ many }) => ({
votes: many(votes),
logs: many(logs),
}));
export const votes = mysqlTable("Vote", {
id: varchar("id", { length: 255 }).notNull().primaryKey(),
created_at: timestamp("created_at", {
mode: "date",
fsp: 3,
}).defaultNow(),
userId: varchar("userId", { length: 255 }).notNull(),
roomId: varchar("roomId", { length: 255 }).notNull(),
value: varchar("value", { length: 255 }).notNull(),
});
export const votesRelations = relations(votes, ({ one }) => ({
room: one(rooms, {
fields: [votes.roomId],
references: [rooms.id],
}),
}));
export const logs = mysqlTable("Log", {
id: varchar("id", { length: 255 }).notNull().primaryKey(),
created_at: timestamp("created_at", {
mode: "date",
fsp: 3,
}).defaultNow(),
userId: varchar("userId", { length: 255 }).notNull(),
roomId: varchar("roomId", { length: 255 }).notNull(),
scale: varchar("scale", { length: 255 }),
votes: json("votes"),
roomName: varchar("roomName", { length: 255 }),
storyName: varchar("storyName", { length: 255 }),
});
export const logsRelations = relations(logs, ({ one }) => ({
room: one(rooms, {
fields: [logs.roomId],
references: [rooms.id],
}),
}));