realworld-hono-drizzle
Version:
A RealWorld backend built with Hono and Drizzle
981 lines (958 loc) • 33.4 kB
JavaScript
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