This commit is contained in:
Atridad Lahiji 2023-08-12 18:17:36 -06:00
parent 92b057993e
commit 047ebe9c8a
No known key found for this signature in database
GPG key ID: 7CB8245F56BC3880
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],
}),
}));