diff --git a/drizzle.config.ts b/drizzle.config.ts index 3297a03..3158962 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -3,7 +3,7 @@ import { type Config } from "drizzle-kit"; import { env } from "@/env"; export default { - schema: "./src/server/db/schema.ts", + schema: "./src/server/db/schema/index.ts", dialect: "postgresql", dbCredentials: { url: env.DATABASE_URL, diff --git a/src/lib/validation/zod/article.ts b/src/lib/validation/zod/article.ts index 01fccf3..da0805b 100644 --- a/src/lib/validation/zod/article.ts +++ b/src/lib/validation/zod/article.ts @@ -1,9 +1,10 @@ import { z } from "zod"; +import { editorContentSchema } from "."; export const articleSchema = z.object({ title: z.string().min(1), slug: z.string().min(1), - content: z.any().optional(), + content: editorContentSchema.optional(), authorId: z.string().optional(), categoryId: z.string().optional(), published: z.boolean(), diff --git a/src/lib/validation/zod/comment.ts b/src/lib/validation/zod/comment.ts new file mode 100644 index 0000000..2bef3b5 --- /dev/null +++ b/src/lib/validation/zod/comment.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; +import { editorContentSchema } from "."; + +export const commentSchema = z.object({ + content: editorContentSchema, + parentId: z.string().optional(), +}); diff --git a/src/lib/validation/zod/index.ts b/src/lib/validation/zod/index.ts new file mode 100644 index 0000000..fce73c2 --- /dev/null +++ b/src/lib/validation/zod/index.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const editorContentSchema = z.any(); // TODO: define editor content schema diff --git a/src/server/actions/auth.ts b/src/server/actions/auth.ts deleted file mode 100644 index 04f6b14..0000000 --- a/src/server/actions/auth.ts +++ /dev/null @@ -1,7 +0,0 @@ -"use server"; - -import { signIn } from "@/server/auth"; - -export async function loginOAuth(provider: string) { - return await signIn(provider); -} diff --git a/src/server/actions/comment.ts b/src/server/actions/comment.ts new file mode 100644 index 0000000..84d4d93 --- /dev/null +++ b/src/server/actions/comment.ts @@ -0,0 +1,43 @@ +"use server"; + +import { appRoutes } from "@/config"; +import { commentSchema } from "@/lib/validation/zod/comment"; +import { api } from "@/trpc/server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +export async function createComment( + comment: z.infer, + articleId: string, +) { + const commentId = await api.comment.create({ + comment, + articleId, + }); + if (!commentId) + return { + success: false, + message: "Error creating comment", + }; + revalidatePath(appRoutes.article(articleId)); + return { + success: true, + }; +} + +export async function deleteComment(commentId: string) { + const articleId = ( + await api.comment.delete({ + commentId, + }) + )?.articleId; + if (!articleId) + return { + success: false, + message: "Error deleting comment", + }; + revalidatePath(appRoutes.article(articleId)); + return { + success: true, + }; +} diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 6ed66b8..268b008 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,22 +1,20 @@ -import { articleRouter } from "./routers/article"; -import { categoryRouter } from "@/server/api/routers/category"; import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc"; -import { usersRouter } from "./routers/users"; -import { authorRouter } from "./routers/author"; -import { appRouter as globalRouter } from "./routers/app"; -// import { authRouter } from "./routers/auth"; -/** - * This is the primary router for your server. - * - * All routers added in /api/routers should be manually added here. - */ +import { + articleRouter, + categoryRouter, + usersRouter, + authorRouter, + appRouter as globalRouter, + commentRouter, +} from "./routers"; + export const appRouter = createTRPCRouter({ article: articleRouter, category: categoryRouter, + comment: commentRouter, users: usersRouter, author: authorRouter, app: globalRouter, - // auth: authRouter, }); // export type definition of API diff --git a/src/server/api/routers/comment.ts b/src/server/api/routers/comment.ts new file mode 100644 index 0000000..38aacd3 --- /dev/null +++ b/src/server/api/routers/comment.ts @@ -0,0 +1,49 @@ +import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { articles, comments } from "@/server/db/schema"; +import { z } from "zod"; +import { commentSchema } from "@/lib/validation/zod/comment"; + +export const commentRouter = createTRPCRouter({ + create: protectedProcedure + .input( + z.object({ + comment: commentSchema, + articleId: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [comment] = await ctx.db + .insert(comments) + .values({ + ...input.comment, + authorId: ctx.session.user.id, + articleId: input.articleId, + }) + .returning({ + id: comments.id, + }); + return comment?.id; + }), + delete: protectedProcedure + .input( + z.object({ + commentId: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [comment] = await ctx.db + .delete(comments) + .where( + and( + eq(comments.id, input.commentId), + eq(comments.authorId, ctx.session.user.id), + ), + ) + .returning({ + id: comments.id, + articleId: comments.articleId, + }); + return comment; + }), +}); diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/index.ts similarity index 73% rename from src/server/api/routers/auth.ts rename to src/server/api/routers/index.ts index a52b113..bd7494c 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/index.ts @@ -1,9 +1,18 @@ -// import { passwordSchema, userSchema } from "@/lib/validation/zod/user"; -// import { createTRPCRouter, publicProcedure } from "../trpc"; -// import { z } from "zod"; -// import { eq } from "drizzle-orm"; -// import { users } from "@/server/db/schema"; -// import argon from "argon2"; +import { articleRouter } from "./article"; +import { usersRouter } from "./users"; +import { authorRouter } from "./author"; +import { appRouter } from "./app"; +import { commentRouter } from "./comment"; +import { categoryRouter } from "./category"; + +export { + articleRouter, + categoryRouter, + commentRouter, + usersRouter, + authorRouter, + appRouter, +}; // export const authRouter = createTRPCRouter({ // register: publicProcedure @@ -52,3 +61,10 @@ // } // }), // }); +// "use server"; + +// import { signIn } from "@/server/auth"; + +// export async function loginOAuth(provider: string) { +// return await signIn(provider); +// } diff --git a/src/server/db/schema/article.ts b/src/server/db/schema/article.ts new file mode 100644 index 0000000..3754cf7 --- /dev/null +++ b/src/server/db/schema/article.ts @@ -0,0 +1,52 @@ +import { boolean, index, jsonb, timestamp, varchar } from "drizzle-orm/pg-core"; +import { createId, createTable } from "./schema-utils"; +import { JSONContent } from "novel"; +import { relations, sql } from "drizzle-orm"; +import { categories, Category } from "./category"; +import { User } from "next-auth"; +import { users } from "./auth"; +import { comments } from "./comments"; + +export const articles = createTable( + "article", + { + id: varchar("id", { length: 255 }) + .primaryKey() + .$defaultFn(() => createId()) + .notNull(), + title: varchar("title", { length: 256 }).notNull(), + slug: varchar("slug", { length: 256 }).unique().notNull(), + authorId: varchar("author_id", { length: 255 }), + content: jsonb("content").$type(), + categoryId: varchar("category_id", { length: 255 }), + published: boolean("published").default(false), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date(), + ), + }, + (example) => ({ + titleIndex: index("article_title_idx").on(example.title), + slugIndex: index("article_slug_idx").on(example.slug), + createdAtIndex: index("article_created_at_idx").on(example.createdAt), + }), +); + +export const articleRelations = relations(articles, ({ one, many }) => ({ + category: one(categories, { + fields: [articles.categoryId], + references: [categories.id], + }), + author: one(users, { + fields: [articles.authorId], + references: [users.id], + }), + comments: many(comments), +})); + +export type Article = typeof articles.$inferSelect & { + author?: User; + category?: Category; +}; diff --git a/src/server/db/schema.ts b/src/server/db/schema/auth.ts similarity index 52% rename from src/server/db/schema.ts rename to src/server/db/schema/auth.ts index 5c5bbc3..c60ae11 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema/auth.ts @@ -1,101 +1,21 @@ -import { createId } from "@paralleldrive/cuid2"; import { relations, sql } from "drizzle-orm"; import { - boolean, index, integer, - jsonb, - pgTableCreator, primaryKey, text, timestamp, varchar, } from "drizzle-orm/pg-core"; -import { User } from "next-auth"; + +import { createId, createTable } from "./schema-utils"; import { type AdapterAccount } from "next-auth/adapters"; -import { JSONContent } from "novel"; - -export const createTable = pgTableCreator((name) => `logipedia_${name}`); - -export const articles = createTable( - "article", - { - id: varchar("id", { length: 255 }) - .primaryKey() - .$defaultFn(() => createId()) - .notNull(), - title: varchar("title", { length: 256 }).notNull(), - slug: varchar("slug", { length: 256 }).unique().notNull(), - authorId: varchar("author_id", { length: 255 }), - content: jsonb("content").$type(), - categoryId: varchar("category_id", { length: 255 }), - published: boolean("published").default(false), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date(), - ), - }, - (example) => ({ - titleIndex: index("article_title_idx").on(example.title), - slugIndex: index("article_slug_idx").on(example.slug), - createdAtIndex: index("article_created_at_idx").on(example.createdAt), - }), -); -export type Article = typeof articles.$inferSelect & { - author?: User; - category?: Category; -}; -export const articleRelations = relations(articles, ({ one }) => ({ - category: one(categories, { - fields: [articles.categoryId], - references: [categories.id], - }), - author: one(users, { - fields: [articles.authorId], - references: [users.id], - }), -})); - -export const categories = createTable( - "category", - { - id: varchar("id", { length: 255 }) - .primaryKey() - .$defaultFn(() => createId()) - .notNull(), - name: varchar("name", { length: 256 }).notNull(), - description: text("description"), - slug: varchar("slug", { length: 256 }).unique().notNull(), - - image: varchar("image", { length: 255 }), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date(), - ), - }, - (example) => ({ - nameIndex: index("category_name_idx").on(example.name), - slugameIndex: index("category_slug_idx").on(example.slug), - createdAtIndex: index("category_created_at_idx").on(example.createdAt), - }), -); -export type Category = typeof categories.$inferSelect & { - articles?: Article[]; -}; - -export const categoryRelations = relations(categories, ({ many }) => ({ - articles: many(articles), -})); export const users = createTable("user", { id: varchar("id", { length: 255 }) .notNull() .primaryKey() - .$defaultFn(() => crypto.randomUUID()), + .$defaultFn(() => createId()), name: varchar("name", { length: 255 }), email: varchar("email", { length: 255 }).notNull(), role: integer("role").default(1).notNull(), diff --git a/src/server/db/schema/category.ts b/src/server/db/schema/category.ts new file mode 100644 index 0000000..bcea946 --- /dev/null +++ b/src/server/db/schema/category.ts @@ -0,0 +1,37 @@ +import { index, text, timestamp, varchar } from "drizzle-orm/pg-core"; +import { createId, createTable } from "./schema-utils"; +import { relations, sql } from "drizzle-orm"; +import { Article, articles } from "./article"; + +export const categories = createTable( + "category", + { + id: varchar("id", { length: 255 }) + .primaryKey() + .$defaultFn(() => createId()) + .notNull(), + name: varchar("name", { length: 256 }).notNull(), + description: text("description"), + slug: varchar("slug", { length: 256 }).unique().notNull(), + + image: varchar("image", { length: 255 }), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date(), + ), + }, + (example) => ({ + nameIndex: index("category_name_idx").on(example.name), + slugameIndex: index("category_slug_idx").on(example.slug), + createdAtIndex: index("category_created_at_idx").on(example.createdAt), + }), +); +export type Category = typeof categories.$inferSelect & { + articles?: Article[]; +}; + +export const categoryRelations = relations(categories, ({ many }) => ({ + articles: many(articles), +})); diff --git a/src/server/db/schema/comments.ts b/src/server/db/schema/comments.ts new file mode 100644 index 0000000..d846032 --- /dev/null +++ b/src/server/db/schema/comments.ts @@ -0,0 +1,39 @@ +import { index, jsonb, timestamp, varchar } from "drizzle-orm/pg-core"; +import { articles } from "./article"; +import { JSONContent } from "novel"; +import { relations, sql } from "drizzle-orm"; +import { createId, createTable } from "./schema-utils"; +import { users } from "./auth"; + +export const comments = createTable( + "comment", + { + id: varchar("id", { length: 255 }) + .primaryKey() + .$defaultFn(() => createId()) + .notNull(), + articleId: varchar("article_id", { length: 255 }) + .notNull() + .references(() => articles.id), + authorId: varchar("author_id", { length: 255 }) + .notNull() + .references(() => users.id), + parentId: varchar("parent_id", { length: 255 }), + content: jsonb("content").$type(), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date(), + ), + }, + (example) => ({ + articleIdIndex: index("comment_article_id_idx").on(example.articleId), + authorIdIndex: index("comment_author_id_idx").on(example.authorId), + createdAtIndex: index("comment_created_at_idx").on(example.createdAt), + }), +); + +export const commentsRelations = relations(comments, ({ many }) => ({ + comments: many(comments), +})); diff --git a/src/server/db/schema/index.ts b/src/server/db/schema/index.ts new file mode 100644 index 0000000..ae0893b --- /dev/null +++ b/src/server/db/schema/index.ts @@ -0,0 +1,4 @@ +export * from "./auth"; +export * from "./article"; +export * from "./category"; +export * from "./comments"; diff --git a/src/server/db/schema/schema-utils.ts b/src/server/db/schema/schema-utils.ts new file mode 100644 index 0000000..9b5062a --- /dev/null +++ b/src/server/db/schema/schema-utils.ts @@ -0,0 +1,7 @@ +import { createId as creatIdCuuid2 } from "@paralleldrive/cuid2"; + +import { pgTableCreator } from "drizzle-orm/pg-core"; + +export const createTable = pgTableCreator((name) => `logipedia_${name}`); + +export const createId = () => creatIdCuuid2();