UNPKG

realworld-hono-drizzle

Version:
981 lines (958 loc) 33.4 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/index.ts import { join } from "path"; import { drizzle as drizzle2 } from "drizzle-orm/libsql"; import { migrate } from "drizzle-orm/libsql/migrator"; // src/db/seed.ts import "dotenv/config"; import slugify from "@sindresorhus/slugify"; import { copycat } from "@snaplet/copycat"; import bcrypt from "bcryptjs"; // src/db/schema.ts var schema_exports = {}; __export(schema_exports, { articleFavoriteTable: () => articleFavoriteTable, articleRelations: () => articleRelations, articleTagRelations: () => articleTagRelations, articleTagTable: () => articleTagTable, articlesTable: () => articlesTable, commentRelations: () => commentRelations, commentsTable: () => commentsTable, tagRelations: () => tagRelations, tagsTable: () => tagsTable, userFollowRelations: () => userFollowRelations, userFollowTable: () => userFollowTable, usersTable: () => usersTable }); import { relations, sql } from "drizzle-orm"; import { customType, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; var date = customType({ dataType() { return "text"; }, fromDriver(value) { const [date2, time] = value.split(" "); return `${date2}T${time}.000Z`; } }); var usersTable = sqliteTable("users", { id: int().primaryKey({ autoIncrement: true }), email: text().notNull().unique(), username: text().notNull().unique(), bio: text(), image: text(), passwordHash: text().notNull() }); var userFollowTable = sqliteTable( "user_follow", { followerId: int().notNull().references(() => usersTable.id, { onDelete: "cascade" }), followedId: int().notNull().references(() => usersTable.id, { onDelete: "cascade" }) }, (t) => [primaryKey({ columns: [t.followerId, t.followedId] })] ); var userFollowRelations = relations(userFollowTable, ({ one }) => ({ follower: one(usersTable, { fields: [userFollowTable.followerId], references: [usersTable.id] }), followed: one(usersTable, { fields: [userFollowTable.followedId], references: [usersTable.id] }) })); var tagsTable = sqliteTable("tags", { tag: text().notNull().unique() }); var articlesTable = sqliteTable("articles", { slug: text().primaryKey(), title: text().notNull(), description: text().notNull(), body: text().notNull(), createdAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`), updatedAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`), authorId: int().notNull().references(() => usersTable.id, { onDelete: "cascade" }) }); var commentsTable = sqliteTable("comments", { id: int().primaryKey({ autoIncrement: true }), createdAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`), updatedAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`), body: text().notNull(), articleSlug: text().notNull().references(() => articlesTable.slug, { onDelete: "cascade", onUpdate: "cascade" }), authorId: int().notNull().references(() => usersTable.id, { onDelete: "cascade" }) }); var articleTagTable = sqliteTable( "article_tag", { articleSlug: text().notNull().references(() => articlesTable.slug, { onDelete: "cascade", onUpdate: "cascade" }), tag: text().notNull().references(() => tagsTable.tag, { onDelete: "cascade" }) }, (t) => [primaryKey({ columns: [t.articleSlug, t.tag] })] ); var articleFavoriteTable = sqliteTable( "article_favorite", { articleSlug: text().notNull().references(() => articlesTable.slug, { onDelete: "cascade", onUpdate: "cascade" }), userId: int().notNull().references(() => usersTable.id, { onDelete: "cascade" }) }, (t) => [primaryKey({ columns: [t.articleSlug, t.userId] })] ); var articleRelations = relations(articlesTable, ({ one, many }) => ({ author: one(usersTable, { fields: [articlesTable.authorId], references: [usersTable.id] }), tagList: many(articleTagTable), commentRelations: many(commentsTable) })); var tagRelations = relations(tagsTable, ({ many }) => ({ articles: many(articleTagTable) })); var articleTagRelations = relations(articleTagTable, ({ one }) => ({ article: one(articlesTable, { fields: [articleTagTable.articleSlug], references: [articlesTable.slug] }), tag: one(tagsTable, { fields: [articleTagTable.tag], references: [tagsTable.tag] }) })); var commentRelations = relations(commentsTable, ({ one }) => ({ article: one(articlesTable, { fields: [commentsTable.articleSlug], references: [articlesTable.slug] }) })); // src/db/seed.ts function seed(db) { return db.transaction(async (db2) => { await db2.delete(tagsTable); await db2.delete(articlesTable); await db2.delete(usersTable); const userCount = 10; const users = Array.from( { length: userCount }, (_, i) => makeUser(`user${i + 1}`) ); const userIds = await db2.insert(usersTable).values(users.map(hashPassword)).returning({ id: usersTable.id }); const articleCount = 30; const articles = Array.from({ length: articleCount }, (_, i) => { const authorId = userIds[i % userCount].id; return { ...makeArticle(`article${i + 1}`), authorId }; }); await db2.insert(articlesTable).values(articles); const tags = Array.from({ length: 7 }, (_, i) => ({ tag: copycat.word(`tag${i + 1}`) })); await db2.insert(tagsTable).values(tags); const comments = []; for (let i = 0; i < 5; i++) { comments.push({ articleSlug: makeArticle("article1").slug, body: copycat.paragraph(`comment${i + 1}_article1`), // biome-ignore lint/style/noNonNullAssertion: the index is always in range authorId: userIds[i % userCount].id }); } for (let i = 0; i < 15; i++) { const articleNum = i % (articleCount - 1) + 2; comments.push({ articleSlug: makeArticle(`article${articleNum}`).slug, body: copycat.paragraph(`comment${i + 6}`), // biome-ignore lint/style/noNonNullAssertion: the index is always in range authorId: userIds[(i + 3) % userCount].id }); } await db2.insert(commentsTable).values(comments); const articleTagRelations2 = []; for (let i = 0; i < articleCount; i++) { const tagCount = i % 3 + 1; for (let j = 0; j < tagCount; j++) { articleTagRelations2.push({ articleSlug: makeArticle(`article${i + 1}`).slug, tag: copycat.word(`tag${(i + j) % 7 + 1}`) }); } } await db2.insert(articleTagTable).values(articleTagRelations2); const favorites = []; for (let i = 0; i < 20; i++) { favorites.push({ articleSlug: makeArticle(`article${i % articleCount + 1}`).slug, // biome-ignore lint/style/noNonNullAssertion: the index is always in range userId: userIds[(i + 2) % userCount].id }); } await db2.insert(articleFavoriteTable).values(favorites); const follows = []; for (let i = 0; i < userCount; i++) { for (let j = 1; j <= 3; j++) { const followedIndex = (i + j) % userCount; if (followedIndex !== i) { follows.push({ // biome-ignore lint/style/noNonNullAssertion: the index is always in range followerId: userIds[i].id, // biome-ignore lint/style/noNonNullAssertion: the index is always in range followedId: userIds[followedIndex].id }); } } } await db2.insert(userFollowTable).values(follows); }); } function makeUser(seedPhrase) { return { email: copycat.email(seedPhrase), username: copycat.username(seedPhrase), password: copycat.password(seedPhrase), bio: copycat.sentence(seedPhrase) }; } function makeArticle(seedPhrase) { const title = copycat.sentence(seedPhrase); const slug = slugify(title); return { slug, title, description: copycat.sentence(seedPhrase), body: Array.from({ length: 5 }, () => copycat.paragraph(seedPhrase)).join( "\n\n" ) }; } function hashPassword(user) { const { password, ...userRest } = user; return { ...userRest, passwordHash: bcrypt.hashSync(password, 10) }; } // src/factory.ts import { drizzle } from "drizzle-orm/libsql"; import { createFactory } from "hono/factory"; // src/modules/articles/articles.ts import { vValidator } from "@hono/valibot-validator"; import slugify2 from "@sindresorhus/slugify"; import { and as and2, countDistinct, desc, eq as eq2, exists as exists2, getTableColumns, sql as sql3 } from "drizzle-orm"; import { Hono } from "hono"; import { decode } from "hono/jwt"; import { array as array2, parse, string as string3 } from "valibot"; // src/auth.ts import { createMiddleware } from "hono/factory"; import { jwt } from "hono/jwt"; import { number, object } from "valibot"; var JwtClaims = object({ id: number() }); var jwtAuth = createMiddleware((c, next) => { const jwtMiddleware = jwt({ secret: c.env.JWT_SECRET }); return jwtMiddleware(c, next); }); var exposeToken = createMiddleware(async (c, next) => { const token = c.req.header("Authorization")?.split(" ")[1]; c.set("token", token); await next(); }); // src/modules/articles/am-i-following.ts import { and, eq, exists, sql as sql2 } from "drizzle-orm"; function amIFollowing({ db, me, them }) { return exists( db.select({ exists: sql2`1` }).from(userFollowTable).where( and( eq(userFollowTable.followerId, me), eq(userFollowTable.followedId, them) ) ) ); } // src/modules/articles/schema.ts import { array, boolean as boolean2, number as number2, object as object3, omit, optional, partial, string as string2 } from "valibot"; // src/modules/profiles/schema.ts import { boolean, nullable, object as object2, string } from "valibot"; var Profile = object2({ username: string(), bio: nullable(string()), image: nullable(string()), following: boolean() }); var ProfileResponse = object2({ profile: Profile }); // src/modules/articles/schema.ts var Article = object3({ slug: string2(), title: string2(), description: string2(), body: string2(), tagList: array(string2()), createdAt: string2(), updatedAt: string2(), favorited: boolean2(), favoritesCount: number2(), author: Profile }); var Comment = object3({ id: number2(), createdAt: string2(), updatedAt: string2(), body: string2(), author: Profile }); var SingleArticleResponse = object3({ article: Article }); var MultipleArticlesResponse = object3({ articles: array(omit(Article, ["body"])), articlesCount: number2() }); var ArticleToCreate = object3({ article: object3({ title: string2(), description: string2(), body: string2(), tagList: optional(array(string2())) }) }); var UpdatedArticle = object3({ article: partial( object3({ title: string2(), description: string2(), body: string2() }) ) }); var SingleCommentResponse = object3({ comment: Comment }); var MultipleCommentsResponse = object3({ comments: array(Comment) }); var CommentToCreate = object3({ comment: object3({ body: string2() }) }); // src/modules/articles/articles.ts var articlesModule = new Hono(); var TagList = array2(string3()); function isFavorited({ db, articleSlug, me }) { return exists2( db.select({ exists: sql3`1` }).from(articleFavoriteTable).where( and2( eq2(articleFavoriteTable.articleSlug, articleSlug), eq2(articleFavoriteTable.userId, me) ) ) ); } async function findArticle(db, slug, self) { const [article] = await db.select({ ...getTableColumns(articlesTable), favorited: (self === null ? sql3`0` : isFavorited({ db, articleSlug: articlesTable.slug, me: self.id })).mapWith(Boolean), favoritesCount: countDistinct(articleFavoriteTable.userId).as( "favoritesCount" ), tagList: sql3`json_group_array(DISTINCT ${articleTagTable.tag}) filter (where ${articleTagTable.tag} is not null)`.mapWith( (tagList) => parse(TagList, JSON.parse(tagList)) ), author: { username: usersTable.username, bio: usersTable.bio, image: usersTable.image, following: (self === null ? sql3`0` : amIFollowing({ db, them: articlesTable.authorId, me: self.id })).mapWith(Boolean) } }).from(articlesTable).leftJoin( articleFavoriteTable, eq2(articlesTable.slug, articleFavoriteTable.articleSlug) ).leftJoin( articleTagTable, eq2(articlesTable.slug, articleTagTable.articleSlug) ).innerJoin(usersTable, eq2(articlesTable.authorId, usersTable.id)).where(eq2(articlesTable.slug, slug)).groupBy(articlesTable.slug); return article; } articlesModule.get("/", exposeToken, async (c) => { const db = c.get("db"); const token = c.get("token"); const self = token !== void 0 ? parse(JwtClaims, decode(token).payload) : null; const tagFilter = c.req.query("tag"); const authorFilter = c.req.query("author"); const favoritedFilter = c.req.query("favorited"); const limit = Number(c.req.query("limit") ?? 20); const offset = Number(c.req.query("offset") ?? 0); const { body: _body, authorId: _authorId, ...desiredColumns } = getTableColumns(articlesTable); const slugsWithTagFilter = tagFilter ? db.select({ slug: articleTagTable.articleSlug }).from(articleTagTable).where(eq2(articleTagTable.tag, tagFilter)).as("slugsWithTagFilter") : null; const slugsWithFavoritedFilter = favoritedFilter ? db.select({ slug: articleFavoriteTable.articleSlug }).from(articleFavoriteTable).innerJoin(usersTable, eq2(articleFavoriteTable.userId, usersTable.id)).where(eq2(usersTable.username, favoritedFilter)).as("slugsWithFavoritedFilter") : null; const articles = await db.select({ ...desiredColumns, favorited: (self === null ? sql3`0` : isFavorited({ db, articleSlug: articlesTable.slug, me: self.id })).mapWith(Boolean), favoritesCount: countDistinct(articleFavoriteTable.userId).as( "favoritesCount" ), tagList: sql3`json_group_array(DISTINCT ${articleTagTable.tag}) filter (where ${articleTagTable.tag} is not null)`.mapWith( (tagList) => parse(TagList, JSON.parse(tagList)) ), author: { username: usersTable.username, bio: usersTable.bio, image: usersTable.image, following: (self === null ? sql3`0` : amIFollowing({ db, them: articlesTable.authorId, me: self.id })).mapWith(Boolean) } }).from(articlesTable).leftJoin( articleFavoriteTable, eq2(articlesTable.slug, articleFavoriteTable.articleSlug) ).leftJoin( articleTagTable, eq2(articlesTable.slug, articleTagTable.articleSlug) ).innerJoin(usersTable, eq2(articlesTable.authorId, usersTable.id)).where( and2( slugsWithTagFilter ? exists2( db.select().from(slugsWithTagFilter).where(eq2(slugsWithTagFilter.slug, articlesTable.slug)) ) : void 0, authorFilter ? eq2(usersTable.username, authorFilter) : void 0, slugsWithFavoritedFilter ? exists2( db.select().from(slugsWithFavoritedFilter).where(eq2(slugsWithFavoritedFilter.slug, articlesTable.slug)) ) : void 0 ) ).limit(limit).offset(offset).groupBy(articlesTable.slug).orderBy(desc(articlesTable.createdAt)); const totalCountResult = await db.select({ count: countDistinct(articlesTable.slug) }).from(articlesTable).leftJoin( articleFavoriteTable, eq2(articlesTable.slug, articleFavoriteTable.articleSlug) ).leftJoin( articleTagTable, eq2(articlesTable.slug, articleTagTable.articleSlug) ).innerJoin(usersTable, eq2(articlesTable.authorId, usersTable.id)).where( and2( slugsWithTagFilter ? exists2( db.select().from(slugsWithTagFilter).where(eq2(slugsWithTagFilter.slug, articlesTable.slug)) ) : void 0, authorFilter ? eq2(usersTable.username, authorFilter) : void 0, slugsWithFavoritedFilter ? exists2( db.select().from(slugsWithFavoritedFilter).where(eq2(slugsWithFavoritedFilter.slug, articlesTable.slug)) ) : void 0 ) ); return c.json( parse(MultipleArticlesResponse, { articles, articlesCount: totalCountResult[0]?.count ?? 0 }) ); }); articlesModule.get("/feed", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const limit = Number(c.req.query("limit") ?? 20); const offset = Number(c.req.query("offset") ?? 0); const { body: _body, authorId: _authorId, ...desiredColumns } = getTableColumns(articlesTable); const articles = await db.select({ ...desiredColumns, favorited: (self === null ? sql3`0` : isFavorited({ db, articleSlug: articlesTable.slug, me: self.id })).mapWith(Boolean), favoritesCount: countDistinct(articleFavoriteTable.userId).as( "favoritesCount" ), tagList: sql3`json_group_array(DISTINCT ${articleTagTable.tag}) filter (where ${articleTagTable.tag} is not null)`.mapWith( (tagList) => parse(TagList, JSON.parse(tagList)) ), author: { username: usersTable.username, bio: usersTable.bio, image: usersTable.image, following: (self === null ? sql3`0` : amIFollowing({ db, them: articlesTable.authorId, me: self.id })).mapWith(Boolean) } }).from(articlesTable).leftJoin( articleFavoriteTable, eq2(articlesTable.slug, articleFavoriteTable.articleSlug) ).leftJoin( articleTagTable, eq2(articlesTable.slug, articleTagTable.articleSlug) ).innerJoin(usersTable, eq2(articlesTable.authorId, usersTable.id)).rightJoin( userFollowTable, eq2(userFollowTable.followedId, articlesTable.authorId) ).where(eq2(userFollowTable.followerId, self.id)).limit(limit).offset(offset).groupBy(articlesTable.slug).orderBy(desc(articlesTable.createdAt)); const totalCountResult = await db.select({ count: countDistinct(articlesTable.slug) }).from(articlesTable).leftJoin( articleFavoriteTable, eq2(articlesTable.slug, articleFavoriteTable.articleSlug) ).leftJoin( articleTagTable, eq2(articlesTable.slug, articleTagTable.articleSlug) ).innerJoin(usersTable, eq2(articlesTable.authorId, usersTable.id)).rightJoin( userFollowTable, eq2(userFollowTable.followedId, articlesTable.authorId) ).where(eq2(userFollowTable.followerId, self.id)); return c.json( parse(MultipleArticlesResponse, { articles, articlesCount: totalCountResult[0]?.count ?? 0 }) ); }); articlesModule.get("/:slug", exposeToken, async (c) => { const db = c.get("db"); const token = c.get("token"); const self = token !== void 0 ? parse(JwtClaims, decode(token).payload) : null; const slug = c.req.param("slug"); const article = await findArticle(db, slug, self); if (article === void 0) { return c.notFound(); } return c.json(parse(SingleArticleResponse, { article })); }); articlesModule.post( "/", jwtAuth, vValidator("json", ArticleToCreate), async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const { tagList, ...articlePayload } = c.req.valid("json").article; const slug = slugify2(articlePayload.title); await db.insert(articlesTable).values({ slug, ...articlePayload, authorId: self.id }); if (tagList !== void 0) { await db.insert(tagsTable).values(tagList.map((tag) => ({ tag }))); await db.insert(articleTagTable).values(tagList.map((tag) => ({ articleSlug: slug, tag }))); } const article = await findArticle(db, slug, self); return c.json(parse(SingleArticleResponse, { article })); } ); articlesModule.put( "/:slug", jwtAuth, vValidator("json", UpdatedArticle), async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); let slug = c.req.param("slug"); const { article: articlePayload } = c.req.valid("json"); const [articleOwnership] = await db.select({ isOwned: eq2(articlesTable.authorId, self.id) }).from(articlesTable).where(eq2(articlesTable.slug, slug)); if (articleOwnership === void 0) { return c.notFound(); } if (!articleOwnership.isOwned) { return new Response("Forbidden", { status: 403 }); } await db.update(articlesTable).set(articlePayload).where(eq2(articlesTable.slug, slug)); if (articlePayload.title !== void 0) { const newSlug = slugify2(articlePayload.title); await db.update(articlesTable).set({ slug: newSlug }).where(eq2(articlesTable.slug, slug)); slug = newSlug; } const article = await findArticle(db, slug, self); return c.json(parse(SingleArticleResponse, { article })); } ); articlesModule.delete("/:slug", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const slug = c.req.param("slug"); const [articleOwnership] = await db.select({ isOwned: eq2(articlesTable.authorId, self.id) }).from(articlesTable).where(eq2(articlesTable.slug, slug)); if (articleOwnership === void 0) { return c.notFound(); } if (!articleOwnership.isOwned) { return new Response("Forbidden", { status: 403 }); } await db.delete(articlesTable).where(eq2(articlesTable.slug, slug)); return new Response(null, { status: 204 }); }); articlesModule.post("/:slug/favorite", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const slug = c.req.param("slug"); const [articleExists] = await db.select({ exists: sql3`1` }).from(articlesTable).where(eq2(articlesTable.slug, slug)); if (articleExists === void 0) { return c.notFound(); } await db.insert(articleFavoriteTable).values({ articleSlug: slug, userId: self.id }).onConflictDoNothing(); const updatedArticle = await findArticle(db, slug, self); return c.json(parse(SingleArticleResponse, { article: updatedArticle })); }); articlesModule.delete("/:slug/favorite", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const slug = c.req.param("slug"); const [articleExists] = await db.select({ exists: sql3`1` }).from(articlesTable).where(eq2(articlesTable.slug, slug)); if (articleExists === void 0) { return c.notFound(); } await db.delete(articleFavoriteTable).where( and2( eq2(articleFavoriteTable.articleSlug, slug), eq2(articleFavoriteTable.userId, self.id) ) ); const updatedArticle = await findArticle(db, slug, self); return c.json(parse(SingleArticleResponse, { article: updatedArticle })); }); // src/modules/articles/comments.ts import { and as and3, eq as eq3, getTableColumns as getTableColumns2, sql as sql4 } from "drizzle-orm"; import { Hono as Hono2 } from "hono"; import { decode as decode2 } from "hono/jwt"; import { parse as parse2 } from "valibot"; import { vValidator as vValidator2 } from "@hono/valibot-validator"; var commentsModule = new Hono2(); commentsModule.get("/:slug/comments", exposeToken, async (c) => { const db = c.get("db"); const token = c.get("token"); const self = token !== void 0 ? parse2(JwtClaims, decode2(token).payload) : null; const slug = c.req.param("slug"); const [articleExists] = await db.select({ exists: sql4`1` }).from(articlesTable).where(eq3(articlesTable.slug, slug)); if (!articleExists) { return c.notFound(); } const { authorId: _authorId, articleSlug: _articleSlug, ...desiredColumns } = getTableColumns2(commentsTable); const comments = await db.select({ ...desiredColumns, author: { username: usersTable.username, bio: usersTable.bio, image: usersTable.image, following: (self === null ? sql4`0` : amIFollowing({ db, them: commentsTable.authorId, me: self.id })).mapWith(Boolean) } }).from(commentsTable).innerJoin(usersTable, eq3(commentsTable.authorId, usersTable.id)).where(eq3(commentsTable.articleSlug, slug)); return c.json(parse2(MultipleCommentsResponse, { comments })); }); commentsModule.post( "/:slug/comments", jwtAuth, vValidator2("json", CommentToCreate), async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const slug = c.req.param("slug"); const [articleExists] = await db.select({ exists: sql4`1` }).from(articlesTable).where(eq3(articlesTable.slug, slug)); if (!articleExists) { return c.notFound(); } const { authorId: _authorId, articleSlug: _articleSlug, ...desiredColumns } = getTableColumns2(commentsTable); const commentPayload = c.req.valid("json").comment; const [addedComment] = await db.insert(commentsTable).values([{ ...commentPayload, articleSlug: slug, authorId: self.id }]).returning(desiredColumns); if (addedComment === void 0) { throw new Error("Failed to insert a comment"); } const selfProfile = await db.query.usersTable.findFirst({ columns: { username: true, bio: true, image: true }, where: eq3(usersTable.id, self.id) }); return c.json( parse2(SingleCommentResponse, { comment: { ...addedComment, author: { ...selfProfile, following: false } } }) ); } ); commentsModule.delete("/:slug/comments/:id", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const slug = c.req.param("slug"); const id = Number.parseInt(c.req.param("id"), 10); const [commentOwnership] = await db.select({ isOwned: eq3(commentsTable.authorId, self.id) }).from(commentsTable).where(and3(eq3(commentsTable.id, id), eq3(commentsTable.articleSlug, slug))); if (commentOwnership === void 0) { return c.notFound(); } if (!commentOwnership.isOwned) { return new Response("Forbidden", { status: 403 }); } await db.delete(commentsTable).where(eq3(commentsTable.id, id)); return new Response(null, { status: 204 }); }); // src/modules/profiles/profiles.ts import { and as and4, eq as eq4 } from "drizzle-orm"; import { Hono as Hono3 } from "hono"; import { parse as parse3 } from "valibot"; import { decode as decode3 } from "hono/jwt"; var profilesModule = new Hono3(); profilesModule.get("/:username", exposeToken, async (c) => { const db = c.get("db"); const token = c.get("token"); const self = token !== void 0 ? parse3(JwtClaims, decode3(token).payload) : null; const user = await db.query.usersTable.findFirst({ where: eq4(usersTable.username, c.req.param("username")) }); if (user === void 0) { return c.notFound(); } const following = self !== null ? await db.query.userFollowTable.findFirst({ where: and4( eq4(userFollowTable.followerId, self.id), eq4(userFollowTable.followedId, user.id) ) }) !== void 0 : false; return c.json(parse3(ProfileResponse, { profile: { ...user, following } })); }); profilesModule.post("/:username/follow", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const userToFollow = await db.query.usersTable.findFirst({ where: eq4(usersTable.username, c.req.param("username")) }); if (userToFollow === void 0) { return c.notFound(); } await db.insert(userFollowTable).values({ followerId: self.id, followedId: userToFollow.id }); return c.json( parse3(ProfileResponse, { profile: { ...userToFollow, following: true } }) ); }); profilesModule.delete("/:username/follow", jwtAuth, async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const userToUnfollow = await db.query.usersTable.findFirst({ where: eq4(usersTable.username, c.req.param("username")) }); if (userToUnfollow === void 0) { return c.notFound(); } await db.delete(userFollowTable).where( and4( eq4(userFollowTable.followerId, self.id), eq4(userFollowTable.followedId, userToUnfollow.id) ) ); return c.json( parse3(ProfileResponse, { profile: { ...userToUnfollow, following: false } }) ); }); // src/modules/tags/tags.ts import { Hono as Hono4 } from "hono"; import { parse as parse4 } from "valibot"; // src/modules/tags/schema.ts import { array as array3, object as object4, string as string4 } from "valibot"; var ListOfTags = object4({ tags: array3(string4()) }); // src/modules/tags/tags.ts var tagsModule = new Hono4(); tagsModule.get("/", async (c) => { const db = c.get("db"); const tags = await db.query.tagsTable.findMany(); return c.json(parse4(ListOfTags, { tags: tags.map(({ tag }) => tag) })); }); // src/modules/users/user.ts import { vValidator as vValidator3 } from "@hono/valibot-validator"; import { eq as eq5 } from "drizzle-orm"; import { Hono as Hono5 } from "hono"; import { parse as parse5 } from "valibot"; // src/modules/users/schema.ts import { url, email, nullable as nullable2, object as object5, partial as partial2, pipe, string as string5 } from "valibot"; var LoginCredentials = object5({ user: object5({ email: pipe(string5(), email()), password: string5() }) }); var RegistrationDetails = object5({ user: object5({ username: string5(), email: pipe(string5(), email()), password: string5() }) }); var UpdatedDetails = object5({ user: partial2( object5({ username: string5(), email: pipe(string5(), email()), password: string5(), bio: nullable2(string5()), image: nullable2(pipe(string5(), url())) }) ) }); var UserResponse = object5({ user: object5({ email: string5(), token: string5(), username: string5(), bio: nullable2(string5()), image: nullable2(string5()) }) }); // src/modules/users/user.ts var userModule = new Hono5().use(jwtAuth).use(exposeToken); userModule.get("/", async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const user = await db.query.usersTable.findFirst({ where: eq5(usersTable.id, self.id) }); if (user === void 0) { return c.notFound(); } return c.json( parse5(UserResponse, { user: { ...user, token: c.get("token") } }) ); }); userModule.put("/", vValidator3("json", UpdatedDetails), async (c) => { const db = c.get("db"); const self = c.get("jwtPayload"); const user = await db.query.usersTable.findFirst({ where: eq5(usersTable.id, self.id) }); if (user === void 0) { return c.notFound(); } const requestData = c.req.valid("json"); const [updatedUser] = await db.update(usersTable).set(requestData.user).where(eq5(usersTable.id, self.id)).returning(); return c.json( parse5(UserResponse, { user: { ...updatedUser, token: c.get("token") } }) ); }); // src/modules/users/users.ts import { vValidator as vValidator4 } from "@hono/valibot-validator"; import bcrypt2 from "bcryptjs"; import { eq as eq6, or } from "drizzle-orm"; import { Hono as Hono6 } from "hono"; import { sign } from "hono/jwt"; import { parse as parse6 } from "valibot"; var usersModule = new Hono6(); usersModule.post("/login", vValidator4("json", LoginCredentials), async (c) => { const db = c.get("db"); const requestData = c.req.valid("json"); const user = await db.query.usersTable.findFirst({ where: eq6(usersTable.email, requestData.user.email) }); const isMatch = user !== void 0 && await bcrypt2.compare(requestData.user.password, user.passwordHash); if (!isMatch) { return c.json({ errors: { password: ["invalid for this email"] } }, 422); } const token = await sign(parse6(JwtClaims, user), c.env.JWT_SECRET); return c.json(parse6(UserResponse, { user: { ...user, token } })); }); usersModule.post("/", vValidator4("json", RegistrationDetails), async (c) => { const db = c.get("db"); const requestData = c.req.valid("json"); const existingUser = await db.query.usersTable.findFirst({ where: or( eq6(usersTable.email, requestData.user.email), eq6(usersTable.username, requestData.user.username) ) }); if (existingUser?.email === requestData.user.email) { return c.json({ errors: { email: ["already in use"] } }, 422); } if (existingUser?.username === requestData.user.username) { return c.json({ errors: { username: ["already in use"] } }, 422); } const passwordHash = await bcrypt2.hash(requestData.user.password, 10); const [user] = await db.insert(usersTable).values({ email: requestData.user.email, username: requestData.user.username, passwordHash }).returning(); const token = await sign(parse6(JwtClaims, user), c.env.JWT_SECRET); return c.json(parse6(UserResponse, { user: { ...user, token } })); }); // src/factory.ts var factory = createFactory({ initApp(app) { app.use(async (c, next) => { const db = drizzle(c.env.DATABASE_URL, { schema: schema_exports }); c.set("db", db); await next(); }); app.route("/api/users", usersModule); app.route("/api/user", userModule); app.route("/api/profiles", profilesModule); app.route("/api/articles", articlesModule); app.route("/api/articles", commentsModule); app.route("/api/tags", tagsModule); } }); // src/index.ts var index_default = factory.createApp(); function applyMigrations(databaseUrl) { return migrate(drizzle2(databaseUrl), { migrationsFolder: join(import.meta.dirname, "../src/db/migrations") }); } function seed2(databaseUrl) { return seed(drizzle2(databaseUrl)); } export { applyMigrations, index_default as default, seed2 as seed }; //# sourceMappingURL=index.js.map