diff --git a/bun.lockb b/bun.lockb index 07bda34c..5b695a10 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d744ea68..c1fe4f22 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ } }, "dependencies": { - "@clerk/nextjs": "^5.2.6", - "@clerk/themes": "^2.1.13", + "@clerk/nextjs": "^5.2.8", + "@clerk/themes": "^2.1.14", "@hookform/resolvers": "^3.9.0", "@neondatabase/serverless": "^0.9.4", "@paralleldrive/cuid2": "^2.2.2", @@ -68,7 +68,7 @@ "@radix-ui/react-slot": "^1.1.0", "@react-email/components": "^0.0.22", "@t3-oss/env-nextjs": "^0.11.0", - "@tanstack/react-query": "^5.51.11", + "@tanstack/react-query": "^5.51.15", "@trpc/client": "next", "@trpc/react-query": "next", "@trpc/server": "next", @@ -104,7 +104,7 @@ "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", "@eslint/compat": "^1.1.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.7.0", + "@eslint/js": "^9.8.0", "@happy-dom/global-registrator": "^14.12.3", "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@next/eslint-plugin-next": "^14.2.5", @@ -121,7 +121,7 @@ "commitizen": "^4.3.0", "cspell": "^8.12.1", "drizzle-kit": "^0.23.0", - "eslint": "^9.7.0", + "eslint": "^9.8.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^48.8.3", "eslint-plugin-jsx-a11y": "^6.9.0", @@ -131,7 +131,7 @@ "eslint-plugin-security": "^3.0.1", "eslint-plugin-tailwindcss": "^3.17.4", "globals": "^15.8.0", - "husky": "^9.1.2", + "husky": "^9.1.3", "lint-staged": "^15.2.7", "markdownlint": "^0.34.0", "markdownlint-cli": "^0.41.0", diff --git a/src/db/actions/modules.ts b/src/db/actions/modules.ts deleted file mode 100644 index 66d69261..00000000 --- a/src/db/actions/modules.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { and, desc, eq } from 'drizzle-orm'; -import { createInsertSchema } from 'drizzle-zod'; -import { db } from '..'; -import { modulesTable } from '../schema'; -import type { z } from 'zod'; - -export const getModuleById = (id: string, userId: string) => - db.query.modulesTable.findFirst({ - where: and(eq(modulesTable.id, id), eq(modulesTable.user_id, userId)), - }); - -export const getModulesByUserId = (userId: string) => - db - .select() - .from(modulesTable) - .where(eq(modulesTable.user_id, userId)) - .orderBy(desc(modulesTable.lastVisited)); - -const insertModuleSchema = createInsertSchema(modulesTable).omit({ - id: true, - createdAt: true, - modifiedAt: true, - lastVisited: true, -}); - -export const createModule = (data: z.infer) => - db.insert(modulesTable).values({ - ...data, - color: data.color ?? 'default', - icon: data.icon ?? 'default', - archived: data.archived ?? false, - credits: data.credits ?? 0, - }); - -export const archiveModule = (id: string, userId: string) => - db - .update(modulesTable) - .set({ archived: true }) - .where(and(eq(modulesTable.id, id), eq(modulesTable.user_id, userId))); - -export const recoverModule = (id: string, userId: string) => - db - .update(modulesTable) - .set({ archived: false }) - .where(and(eq(modulesTable.id, id), eq(modulesTable.user_id, userId))); - -export const updateModule = async ( - id: string, - userId: string, - data: z.infer, -) => { - const existingModule = await getModuleById(id, userId); - - if (!existingModule) { - throw new Error('Module not found'); - } - - return db - .update(modulesTable) - .set({ - ...data, - color: data.color ?? existingModule.color, - icon: data.icon ?? existingModule.icon, - archived: data.archived ?? existingModule.archived, - credits: data.credits ?? existingModule.credits, - }) - .where(and(eq(modulesTable.id, id), eq(modulesTable.user_id, userId))); -}; - -export const deleteModule = (id: string, userId: string) => - db - .delete(modulesTable) - .where(and(eq(modulesTable.id, id), eq(modulesTable.user_id, userId))); - -export const updateLastVisited = (id: string, userId: string) => - db - .update(modulesTable) - .set({ lastVisited: new Date() }) - .where(and(eq(modulesTable.id, id), eq(modulesTable.user_id, userId))); diff --git a/src/db/schema/modules.ts b/src/db/schema/modules.ts index 7f63eaa5..2ae35d78 100644 --- a/src/db/schema/modules.ts +++ b/src/db/schema/modules.ts @@ -6,6 +6,8 @@ import { timestamp, uuid, } from 'drizzle-orm/pg-core'; +import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; +import { z } from 'zod'; export const modulesTable = pgTable('modules', { id: uuid('id').primaryKey().unique().defaultRandom().notNull(), @@ -21,3 +23,18 @@ export const modulesTable = pgTable('modules', { modifiedAt: timestamp('modified_at').notNull().defaultNow(), lastVisited: timestamp('last_visited').notNull().defaultNow(), }); + +export const insertModuleSchema = createInsertSchema(modulesTable).extend({ + id: z.string().min(1), + icon: z.string().default('default'), + color: z.string().default('default'), + archived: z.boolean().default(false), + credits: z.number().default(0), + createdAt: z.date().default(new Date()), + modifiedAt: z.date().default(new Date()), + lastVisited: z.date().default(new Date()), +}); + +export type InsertModuleInput = z.infer; + +export const selectModuleSchema = createSelectSchema(modulesTable); diff --git a/src/server/index.ts b/src/server/index.ts index 0b272494..64fd29d5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,8 +1,10 @@ import { earlyAccessRouter } from './routers/early-access'; +import { modulesRouter } from './routers/modules'; import { createCallerFactory, createRouter } from './trpc'; export const appRouter = createRouter({ earlyAccess: earlyAccessRouter, + modules: modulesRouter, }); export type AppRouter = typeof appRouter; diff --git a/src/server/routers/modules.ts b/src/server/routers/modules.ts new file mode 100644 index 00000000..afdc1c39 --- /dev/null +++ b/src/server/routers/modules.ts @@ -0,0 +1,125 @@ +import { createRouter, protectedProcedure } from '../trpc'; +import { + insertModuleSchema, + modulesTable, + selectModuleSchema, +} from '@/db/schema'; +import { and, eq } from 'drizzle-orm'; + +export const modulesRouter = createRouter({ + getById: protectedProcedure + .input(selectModuleSchema.pick({ id: true })) + .query(async ({ ctx, input }) => { + const userId = ctx.user.id; + + return ctx.db.query.modulesTable.findFirst({ + where: (t, { and, eq }) => + and(eq(t.id, input.id), eq(t.user_id, userId)), + }); + }), + + getUserModules: protectedProcedure.query(async ({ ctx }) => { + const userId = ctx.user.id; + + return ctx.db.query.modulesTable.findMany({ + where: (t, { eq }) => eq(t.user_id, userId), + orderBy: (t, { desc }) => desc(t.lastVisited), + }); + }), + + create: protectedProcedure + .input( + insertModuleSchema.pick({ + name: true, + description: true, + code: true, + icon: true, + color: true, + credits: true, + }), + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user.id; + + return ctx.db.insert(modulesTable).values({ + ...input, + user_id: userId, + }); + }), + + archive: protectedProcedure + .input(selectModuleSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user.id; + + return ctx.db + .update(modulesTable) + .set({ archived: true }) + .where( + and(eq(modulesTable.id, input.id), eq(modulesTable.user_id, userId)), + ); + }), + + recover: protectedProcedure + .input(selectModuleSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user.id; + + return ctx.db + .update(modulesTable) + .set({ archived: false }) + .where( + and(eq(modulesTable.id, input.id), eq(modulesTable.user_id, userId)), + ); + }), + + update: protectedProcedure + .input( + insertModuleSchema.pick({ + color: true, + icon: true, + name: true, + description: true, + code: true, + credits: true, + id: true, + }), + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user.id; + + const { id: moduleId, ...updateData } = input; + + const existingModule = await ctx.db.query.modulesTable.findFirst({ + where: (t, { and, eq }) => + and(eq(t.id, moduleId), eq(t.user_id, userId)), + }); + + if (!existingModule) { + throw new Error('Module not found'); + } + + return ctx.db + .update(modulesTable) + .set({ + ...updateData, + modifiedAt: new Date(), + }) + .where( + and(eq(modulesTable.id, moduleId), eq(modulesTable.user_id, userId)), + ); + }), + + updateLastVisited: protectedProcedure + .input(insertModuleSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user.id; + + return ctx.db + .update(modulesTable) + .set({ lastVisited: new Date() }) + .where( + and(eq(modulesTable.id, input.id), eq(modulesTable.user_id, userId)), + ); + }), +}); diff --git a/src/server/trpc.ts b/src/server/trpc.ts index c0b84dac..74ff0556 100644 --- a/src/server/trpc.ts +++ b/src/server/trpc.ts @@ -35,15 +35,15 @@ export const createRouter = t.router; export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(async ({ next }) => { - const session = await currentUser(); + const user = await currentUser(); - if (!session) { + if (!user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { - session: { ...session }, + user: { ...user }, }, }); });