UNPKG

realworld-hono-drizzle

Version:
1 lines 64.5 kB
{"version":3,"sources":["../src/index.ts","../src/db/seed.ts","../src/db/schema.ts","../src/factory.ts","../src/modules/articles/articles.ts","../src/auth.ts","../src/modules/articles/am-i-following.ts","../src/modules/articles/schema.ts","../src/modules/profiles/schema.ts","../src/modules/articles/comments.ts","../src/modules/profiles/profiles.ts","../src/modules/tags/tags.ts","../src/modules/tags/schema.ts","../src/modules/users/user.ts","../src/modules/users/schema.ts","../src/modules/users/users.ts"],"sourcesContent":["import { join } from \"node:path\";\nimport { drizzle } from \"drizzle-orm/libsql\";\nimport { migrate } from \"drizzle-orm/libsql/migrator\";\n\nimport { seed as seedInternal } from \"./db/seed.js\";\nimport { factory } from \"./factory.js\";\n\nexport default factory.createApp();\n\n/** Create the tables in the given database. */\nexport function applyMigrations(databaseUrl: string): Promise<void> {\n\treturn migrate(drizzle(databaseUrl), {\n\t\tmigrationsFolder: join(import.meta.dirname, \"../src/db/migrations\"),\n\t});\n}\n\n/** Populate the database with fake values. */\nexport function seed(databaseUrl: string): Promise<void> {\n\treturn seedInternal(drizzle(databaseUrl));\n}\n","import \"dotenv/config\";\nimport slugify from \"@sindresorhus/slugify\";\nimport { copycat } from \"@snaplet/copycat\";\nimport bcrypt from \"bcryptjs\";\nimport type { LibSQLDatabase } from \"drizzle-orm/libsql\";\n\nimport * as schema from \"./schema.js\";\n\nexport function seed(db: LibSQLDatabase) {\n\treturn db.transaction(async (db) => {\n\t\tawait db.delete(schema.tagsTable);\n\t\tawait db.delete(schema.articlesTable);\n\t\tawait db.delete(schema.usersTable);\n\n\t\t// Create 10 users\n\t\tconst userCount = 10;\n\t\tconst users = Array.from({ length: userCount }, (_, i) =>\n\t\t\tmakeUser(`user${i + 1}`),\n\t\t);\n\n\t\tconst userIds = (await db\n\t\t\t.insert(schema.usersTable)\n\t\t\t.values(users.map(hashPassword))\n\t\t\t.returning({ id: schema.usersTable.id })) as Array<{ id: number }>;\n\n\t\t// Create 30 articles distributed among users\n\t\tconst articleCount = 30;\n\t\tconst articles = Array.from({ length: articleCount }, (_, i) => {\n\t\t\t// biome-ignore lint/style/noNonNullAssertion: the index is always in range\n\t\t\tconst authorId = userIds[i % userCount]!.id;\n\t\t\treturn { ...makeArticle(`article${i + 1}`), authorId };\n\t\t});\n\n\t\tawait db.insert(schema.articlesTable).values(articles);\n\n\t\t// Create 7 tags\n\t\tconst tags = Array.from({ length: 7 }, (_, i) => ({\n\t\t\ttag: copycat.word(`tag${i + 1}`),\n\t\t}));\n\n\t\tawait db.insert(schema.tagsTable).values(tags);\n\n\t\t// Create 20 comments, with 5 on one article (article1)\n\t\tconst comments = [];\n\t\t// 5 comments on article1\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tcomments.push({\n\t\t\t\tarticleSlug: makeArticle(\"article1\").slug,\n\t\t\t\tbody: copycat.paragraph(`comment${i + 1}_article1`),\n\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: the index is always in range\n\t\t\t\tauthorId: userIds[i % userCount]!.id,\n\t\t\t});\n\t\t}\n\n\t\t// 15 more comments distributed among other articles\n\t\tfor (let i = 0; i < 15; i++) {\n\t\t\tconst articleNum = (i % (articleCount - 1)) + 2; // Skip article1 (starts from article2)\n\t\t\tcomments.push({\n\t\t\t\tarticleSlug: makeArticle(`article${articleNum}`).slug,\n\t\t\t\tbody: copycat.paragraph(`comment${i + 6}`),\n\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: the index is always in range\n\t\t\t\tauthorId: userIds[(i + 3) % userCount]!.id,\n\t\t\t});\n\t\t}\n\n\t\tawait db.insert(schema.commentsTable).values(comments);\n\n\t\t// Create article-tag relationships (each article gets 1-3 random tags)\n\t\tconst articleTagRelations = [];\n\t\tfor (let i = 0; i < articleCount; i++) {\n\t\t\tconst tagCount = (i % 3) + 1; // 1-3 tags per article\n\t\t\tfor (let j = 0; j < tagCount; j++) {\n\t\t\t\tarticleTagRelations.push({\n\t\t\t\t\tarticleSlug: makeArticle(`article${i + 1}`).slug,\n\t\t\t\t\ttag: copycat.word(`tag${((i + j) % 7) + 1}`),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tawait db.insert(schema.articleTagTable).values(articleTagRelations);\n\n\t\t// Create article favorites (some users favorite some articles)\n\t\tconst favorites = [];\n\t\tfor (let i = 0; i < 20; i++) {\n\t\t\tfavorites.push({\n\t\t\t\tarticleSlug: makeArticle(`article${(i % articleCount) + 1}`).slug,\n\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: the index is always in range\n\t\t\t\tuserId: userIds[(i + 2) % userCount]!.id,\n\t\t\t});\n\t\t}\n\n\t\tawait db.insert(schema.articleFavoriteTable).values(favorites);\n\n\t\t// Create user follows (create a network of followers)\n\t\tconst follows = [];\n\t\tfor (let i = 0; i < userCount; i++) {\n\t\t\t// Each user follows 3 other users (modulo logic to avoid self-follows)\n\t\t\tfor (let j = 1; j <= 3; j++) {\n\t\t\t\tconst followedIndex = (i + j) % userCount;\n\t\t\t\tif (followedIndex !== i) {\n\t\t\t\t\t// Avoid self-follows\n\t\t\t\t\tfollows.push({\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: the index is always in range\n\t\t\t\t\t\tfollowerId: userIds[i]!.id,\n\t\t\t\t\t\t// biome-ignore lint/style/noNonNullAssertion: the index is always in range\n\t\t\t\t\t\tfollowedId: userIds[followedIndex]!.id,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tawait db.insert(schema.userFollowTable).values(follows);\n\t});\n}\n\nfunction makeUser(seedPhrase: string) {\n\treturn {\n\t\temail: copycat.email(seedPhrase),\n\t\tusername: copycat.username(seedPhrase),\n\t\tpassword: copycat.password(seedPhrase),\n\t\tbio: copycat.sentence(seedPhrase),\n\t};\n}\n\nfunction makeArticle(seedPhrase: string) {\n\tconst title = copycat.sentence(seedPhrase);\n\tconst slug = slugify(title);\n\treturn {\n\t\tslug,\n\t\ttitle,\n\t\tdescription: copycat.sentence(seedPhrase),\n\t\tbody: Array.from({ length: 5 }, () => copycat.paragraph(seedPhrase)).join(\n\t\t\t\"\\n\\n\",\n\t\t),\n\t};\n}\n\nfunction hashPassword(user: ReturnType<typeof makeUser>) {\n\tconst { password, ...userRest } = user;\n\treturn { ...userRest, passwordHash: bcrypt.hashSync(password, 10) };\n}\n","import { relations, sql } from \"drizzle-orm\";\nimport {\n\tcustomType,\n\tint,\n\tprimaryKey,\n\tsqliteTable,\n\ttext,\n} from \"drizzle-orm/sqlite-core\";\n\nconst date = customType<{\n\tdata: string;\n\tdriverData: string;\n}>({\n\tdataType() {\n\t\treturn \"text\";\n\t},\n\tfromDriver(value: string): string {\n\t\tconst [date, time] = value.split(\" \");\n\t\treturn `${date}T${time}.000Z`;\n\t},\n});\n\nexport const usersTable = sqliteTable(\"users\", {\n\tid: int().primaryKey({ autoIncrement: true }),\n\temail: text().notNull().unique(),\n\tusername: text().notNull().unique(),\n\tbio: text(),\n\timage: text(),\n\tpasswordHash: text().notNull(),\n});\n\nexport const userFollowTable = sqliteTable(\n\t\"user_follow\",\n\t{\n\t\tfollowerId: int()\n\t\t\t.notNull()\n\t\t\t.references(() => usersTable.id, { onDelete: \"cascade\" }),\n\t\tfollowedId: int()\n\t\t\t.notNull()\n\t\t\t.references(() => usersTable.id, { onDelete: \"cascade\" }),\n\t},\n\t(t) => [primaryKey({ columns: [t.followerId, t.followedId] })],\n);\n\nexport const userFollowRelations = relations(userFollowTable, ({ one }) => ({\n\tfollower: one(usersTable, {\n\t\tfields: [userFollowTable.followerId],\n\t\treferences: [usersTable.id],\n\t}),\n\tfollowed: one(usersTable, {\n\t\tfields: [userFollowTable.followedId],\n\t\treferences: [usersTable.id],\n\t}),\n}));\n\nexport const tagsTable = sqliteTable(\"tags\", {\n\ttag: text().notNull().unique(),\n});\n\nexport const articlesTable = sqliteTable(\"articles\", {\n\tslug: text().primaryKey(),\n\ttitle: text().notNull(),\n\tdescription: text().notNull(),\n\tbody: text().notNull(),\n\tcreatedAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`),\n\tupdatedAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`),\n\tauthorId: int()\n\t\t.notNull()\n\t\t.references(() => usersTable.id, { onDelete: \"cascade\" }),\n});\n\nexport const commentsTable = sqliteTable(\"comments\", {\n\tid: int().primaryKey({ autoIncrement: true }),\n\tcreatedAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`),\n\tupdatedAt: date().notNull().default(sql`(CURRENT_TIMESTAMP)`),\n\tbody: text().notNull(),\n\tarticleSlug: text()\n\t\t.notNull()\n\t\t.references(() => articlesTable.slug, {\n\t\t\tonDelete: \"cascade\",\n\t\t\tonUpdate: \"cascade\",\n\t\t}),\n\tauthorId: int()\n\t\t.notNull()\n\t\t.references(() => usersTable.id, { onDelete: \"cascade\" }),\n});\n\nexport const articleTagTable = sqliteTable(\n\t\"article_tag\",\n\t{\n\t\tarticleSlug: text()\n\t\t\t.notNull()\n\t\t\t.references(() => articlesTable.slug, {\n\t\t\t\tonDelete: \"cascade\",\n\t\t\t\tonUpdate: \"cascade\",\n\t\t\t}),\n\t\ttag: text()\n\t\t\t.notNull()\n\t\t\t.references(() => tagsTable.tag, { onDelete: \"cascade\" }),\n\t},\n\t(t) => [primaryKey({ columns: [t.articleSlug, t.tag] })],\n);\n\nexport const articleFavoriteTable = sqliteTable(\n\t\"article_favorite\",\n\t{\n\t\tarticleSlug: text()\n\t\t\t.notNull()\n\t\t\t.references(() => articlesTable.slug, {\n\t\t\t\tonDelete: \"cascade\",\n\t\t\t\tonUpdate: \"cascade\",\n\t\t\t}),\n\t\tuserId: int()\n\t\t\t.notNull()\n\t\t\t.references(() => usersTable.id, { onDelete: \"cascade\" }),\n\t},\n\t(t) => [primaryKey({ columns: [t.articleSlug, t.userId] })],\n);\n\nexport const articleRelations = relations(articlesTable, ({ one, many }) => ({\n\tauthor: one(usersTable, {\n\t\tfields: [articlesTable.authorId],\n\t\treferences: [usersTable.id],\n\t}),\n\ttagList: many(articleTagTable),\n\tcommentRelations: many(commentsTable),\n}));\n\nexport const tagRelations = relations(tagsTable, ({ many }) => ({\n\tarticles: many(articleTagTable),\n}));\n\nexport const articleTagRelations = relations(articleTagTable, ({ one }) => ({\n\tarticle: one(articlesTable, {\n\t\tfields: [articleTagTable.articleSlug],\n\t\treferences: [articlesTable.slug],\n\t}),\n\ttag: one(tagsTable, {\n\t\tfields: [articleTagTable.tag],\n\t\treferences: [tagsTable.tag],\n\t}),\n}));\n\nexport const commentRelations = relations(commentsTable, ({ one }) => ({\n\tarticle: one(articlesTable, {\n\t\tfields: [commentsTable.articleSlug],\n\t\treferences: [articlesTable.slug],\n\t}),\n}));\n","import { type LibSQLDatabase, drizzle } from \"drizzle-orm/libsql\";\nimport { createFactory } from \"hono/factory\";\n\nimport * as schema from \"./db/schema.js\";\nimport { articlesModule } from \"./modules/articles/articles.js\";\nimport { commentsModule } from \"./modules/articles/comments.js\";\nimport { profilesModule } from \"./modules/profiles/profiles.js\";\nimport { tagsModule } from \"./modules/tags/tags.js\";\nimport { userModule } from \"./modules/users/user.js\";\nimport { usersModule } from \"./modules/users/users.js\";\n\nexport interface ThisAppEnv {\n\tVariables: { db: LibSQLDatabase<typeof schema> };\n\tBindings: {\n\t\tDATABASE_URL: string;\n\t\tJWT_SECRET: string;\n\t};\n}\n\nexport const factory = createFactory<ThisAppEnv>({\n\tinitApp(app) {\n\t\tapp.use(async (c, next) => {\n\t\t\tconst db = drizzle(c.env.DATABASE_URL, { schema });\n\t\t\tc.set(\"db\", db);\n\t\t\tawait next();\n\t\t});\n\n\t\tapp.route(\"/api/users\", usersModule);\n\t\tapp.route(\"/api/user\", userModule);\n\t\tapp.route(\"/api/profiles\", profilesModule);\n\t\tapp.route(\"/api/articles\", articlesModule);\n\t\tapp.route(\"/api/articles\", commentsModule);\n\t\tapp.route(\"/api/tags\", tagsModule);\n\t},\n});\n","import { vValidator } from \"@hono/valibot-validator\";\nimport slugify from \"@sindresorhus/slugify\";\nimport {\n\tand,\n\tcountDistinct,\n\tdesc,\n\teq,\n\texists,\n\tgetTableColumns,\n\tsql,\n} from \"drizzle-orm\";\nimport type { LibSQLDatabase } from \"drizzle-orm/libsql\";\nimport type { SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport { Hono } from \"hono\";\nimport { decode } from \"hono/jwt\";\nimport { type InferOutput, array, parse, string } from \"valibot\";\n\nimport { JwtClaims, exposeToken, jwtAuth } from \"../../auth.js\";\nimport type * as schema from \"../../db/schema.js\";\nimport {\n\tarticleFavoriteTable,\n\tarticleTagTable,\n\tarticlesTable,\n\ttagsTable,\n\tuserFollowTable,\n\tusersTable,\n} from \"../../db/schema.js\";\nimport type { ThisAppEnv } from \"../../factory.js\";\nimport { amIFollowing } from \"./am-i-following.js\";\nimport {\n\tArticleToCreate,\n\tMultipleArticlesResponse,\n\tSingleArticleResponse,\n\tUpdatedArticle,\n} from \"./schema.js\";\n\nexport const articlesModule = new Hono<ThisAppEnv>();\n\nconst TagList = array(string());\n\n/** Subquery to include the `favorited` field on an article. */\nfunction isFavorited({\n\tdb,\n\tarticleSlug,\n\tme,\n}: {\n\tdb: LibSQLDatabase<typeof schema>;\n\tarticleSlug: SQLiteColumn;\n\tme: number;\n}) {\n\treturn exists(\n\t\tdb\n\t\t\t.select({ exists: sql`1` })\n\t\t\t.from(articleFavoriteTable)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\teq(articleFavoriteTable.articleSlug, articleSlug),\n\t\t\t\t\teq(articleFavoriteTable.userId, me),\n\t\t\t\t),\n\t\t\t),\n\t);\n}\n\nasync function findArticle(\n\tdb: LibSQLDatabase<typeof schema>,\n\tslug: string,\n\tself: InferOutput<typeof JwtClaims> | null,\n) {\n\tconst [article] = await db\n\t\t.select({\n\t\t\t...getTableColumns(articlesTable),\n\t\t\tfavorited: (self === null\n\t\t\t\t? sql<number>`0`\n\t\t\t\t: isFavorited({ db, articleSlug: articlesTable.slug, me: self.id })\n\t\t\t).mapWith(Boolean),\n\t\t\tfavoritesCount: countDistinct(articleFavoriteTable.userId).as(\n\t\t\t\t\"favoritesCount\",\n\t\t\t),\n\t\t\ttagList:\n\t\t\t\tsql<string>`json_group_array(DISTINCT ${articleTagTable.tag}) filter (where ${articleTagTable.tag} is not null)`.mapWith(\n\t\t\t\t\t(tagList) => parse(TagList, JSON.parse(tagList)),\n\t\t\t\t),\n\t\t\tauthor: {\n\t\t\t\tusername: usersTable.username,\n\t\t\t\tbio: usersTable.bio,\n\t\t\t\timage: usersTable.image,\n\t\t\t\tfollowing: (self === null\n\t\t\t\t\t? sql<number>`0`\n\t\t\t\t\t: amIFollowing({ db, them: articlesTable.authorId, me: self.id })\n\t\t\t\t).mapWith(Boolean),\n\t\t\t},\n\t\t})\n\t\t.from(articlesTable)\n\t\t.leftJoin(\n\t\t\tarticleFavoriteTable,\n\t\t\teq(articlesTable.slug, articleFavoriteTable.articleSlug),\n\t\t)\n\t\t.leftJoin(\n\t\t\tarticleTagTable,\n\t\t\teq(articlesTable.slug, articleTagTable.articleSlug),\n\t\t)\n\t\t.innerJoin(usersTable, eq(articlesTable.authorId, usersTable.id))\n\t\t.where(eq(articlesTable.slug, slug))\n\t\t.groupBy(articlesTable.slug);\n\n\treturn article;\n}\n\narticlesModule.get(\"/\", exposeToken, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst token = c.get(\"token\");\n\tconst self =\n\t\ttoken !== undefined ? parse(JwtClaims, decode(token).payload) : null;\n\n\tconst tagFilter = c.req.query(\"tag\");\n\tconst authorFilter = c.req.query(\"author\");\n\tconst favoritedFilter = c.req.query(\"favorited\");\n\tconst limit = Number(c.req.query(\"limit\") ?? 20);\n\tconst offset = Number(c.req.query(\"offset\") ?? 0);\n\n\tconst {\n\t\tbody: _body,\n\t\tauthorId: _authorId,\n\t\t...desiredColumns\n\t} = getTableColumns(articlesTable);\n\n\tconst slugsWithTagFilter = tagFilter\n\t\t? db\n\t\t\t\t.select({ slug: articleTagTable.articleSlug })\n\t\t\t\t.from(articleTagTable)\n\t\t\t\t.where(eq(articleTagTable.tag, tagFilter))\n\t\t\t\t.as(\"slugsWithTagFilter\")\n\t\t: null;\n\tconst slugsWithFavoritedFilter = favoritedFilter\n\t\t? db\n\t\t\t\t.select({ slug: articleFavoriteTable.articleSlug })\n\t\t\t\t.from(articleFavoriteTable)\n\t\t\t\t.innerJoin(usersTable, eq(articleFavoriteTable.userId, usersTable.id))\n\t\t\t\t.where(eq(usersTable.username, favoritedFilter))\n\t\t\t\t.as(\"slugsWithFavoritedFilter\")\n\t\t: null;\n\n\tconst articles = await db\n\t\t.select({\n\t\t\t...desiredColumns,\n\t\t\tfavorited: (self === null\n\t\t\t\t? sql<number>`0`\n\t\t\t\t: isFavorited({ db, articleSlug: articlesTable.slug, me: self.id })\n\t\t\t).mapWith(Boolean),\n\t\t\tfavoritesCount: countDistinct(articleFavoriteTable.userId).as(\n\t\t\t\t\"favoritesCount\",\n\t\t\t),\n\t\t\ttagList:\n\t\t\t\tsql<string>`json_group_array(DISTINCT ${articleTagTable.tag}) filter (where ${articleTagTable.tag} is not null)`.mapWith(\n\t\t\t\t\t(tagList) => parse(TagList, JSON.parse(tagList)),\n\t\t\t\t),\n\t\t\tauthor: {\n\t\t\t\tusername: usersTable.username,\n\t\t\t\tbio: usersTable.bio,\n\t\t\t\timage: usersTable.image,\n\t\t\t\tfollowing: (self === null\n\t\t\t\t\t? sql<number>`0`\n\t\t\t\t\t: amIFollowing({ db, them: articlesTable.authorId, me: self.id })\n\t\t\t\t).mapWith(Boolean),\n\t\t\t},\n\t\t})\n\t\t.from(articlesTable)\n\t\t.leftJoin(\n\t\t\tarticleFavoriteTable,\n\t\t\teq(articlesTable.slug, articleFavoriteTable.articleSlug),\n\t\t)\n\t\t.leftJoin(\n\t\t\tarticleTagTable,\n\t\t\teq(articlesTable.slug, articleTagTable.articleSlug),\n\t\t)\n\t\t.innerJoin(usersTable, eq(articlesTable.authorId, usersTable.id))\n\t\t.where(\n\t\t\tand(\n\t\t\t\tslugsWithTagFilter\n\t\t\t\t\t? exists(\n\t\t\t\t\t\t\tdb\n\t\t\t\t\t\t\t\t.select()\n\t\t\t\t\t\t\t\t.from(slugsWithTagFilter)\n\t\t\t\t\t\t\t\t.where(eq(slugsWithTagFilter.slug, articlesTable.slug)),\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined,\n\t\t\t\tauthorFilter ? eq(usersTable.username, authorFilter) : undefined,\n\t\t\t\tslugsWithFavoritedFilter\n\t\t\t\t\t? exists(\n\t\t\t\t\t\t\tdb\n\t\t\t\t\t\t\t\t.select()\n\t\t\t\t\t\t\t\t.from(slugsWithFavoritedFilter)\n\t\t\t\t\t\t\t\t.where(eq(slugsWithFavoritedFilter.slug, articlesTable.slug)),\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined,\n\t\t\t),\n\t\t)\n\t\t.limit(limit)\n\t\t.offset(offset)\n\t\t.groupBy(articlesTable.slug)\n\t\t.orderBy(desc(articlesTable.createdAt));\n\n\treturn c.json(\n\t\tparse(MultipleArticlesResponse, {\n\t\t\tarticles,\n\t\t\tarticlesCount: articles.length,\n\t\t}),\n\t);\n});\n\narticlesModule.get(\"/feed\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\n\tconst limit = Number(c.req.query(\"limit\") ?? 20);\n\tconst offset = Number(c.req.query(\"offset\") ?? 0);\n\n\tconst {\n\t\tbody: _body,\n\t\tauthorId: _authorId,\n\t\t...desiredColumns\n\t} = getTableColumns(articlesTable);\n\n\tconst articles = await db\n\t\t.select({\n\t\t\t...desiredColumns,\n\t\t\tfavorited: (self === null\n\t\t\t\t? sql<number>`0`\n\t\t\t\t: isFavorited({ db, articleSlug: articlesTable.slug, me: self.id })\n\t\t\t).mapWith(Boolean),\n\t\t\tfavoritesCount: countDistinct(articleFavoriteTable.userId).as(\n\t\t\t\t\"favoritesCount\",\n\t\t\t),\n\t\t\ttagList:\n\t\t\t\tsql<string>`json_group_array(DISTINCT ${articleTagTable.tag}) filter (where ${articleTagTable.tag} is not null)`.mapWith(\n\t\t\t\t\t(tagList) => parse(TagList, JSON.parse(tagList)),\n\t\t\t\t),\n\t\t\tauthor: {\n\t\t\t\tusername: usersTable.username,\n\t\t\t\tbio: usersTable.bio,\n\t\t\t\timage: usersTable.image,\n\t\t\t\tfollowing: (self === null\n\t\t\t\t\t? sql<number>`0`\n\t\t\t\t\t: amIFollowing({ db, them: articlesTable.authorId, me: self.id })\n\t\t\t\t).mapWith(Boolean),\n\t\t\t},\n\t\t})\n\t\t.from(articlesTable)\n\t\t.leftJoin(\n\t\t\tarticleFavoriteTable,\n\t\t\teq(articlesTable.slug, articleFavoriteTable.articleSlug),\n\t\t)\n\t\t.leftJoin(\n\t\t\tarticleTagTable,\n\t\t\teq(articlesTable.slug, articleTagTable.articleSlug),\n\t\t)\n\t\t.innerJoin(usersTable, eq(articlesTable.authorId, usersTable.id))\n\t\t.rightJoin(\n\t\t\tuserFollowTable,\n\t\t\teq(userFollowTable.followedId, articlesTable.authorId),\n\t\t)\n\t\t.where(eq(userFollowTable.followerId, self.id))\n\t\t.limit(limit)\n\t\t.offset(offset)\n\t\t.groupBy(articlesTable.slug)\n\t\t.orderBy(desc(articlesTable.createdAt));\n\n\treturn c.json(\n\t\tparse(MultipleArticlesResponse, {\n\t\t\tarticles,\n\t\t\tarticlesCount: articles.length,\n\t\t}),\n\t);\n});\n\narticlesModule.get(\"/:slug\", exposeToken, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst token = c.get(\"token\");\n\tconst self =\n\t\ttoken !== undefined ? parse(JwtClaims, decode(token).payload) : null;\n\n\tconst slug = c.req.param(\"slug\");\n\n\tconst article = await findArticle(db, slug, self);\n\n\tif (article === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\treturn c.json(parse(SingleArticleResponse, { article }));\n});\n\narticlesModule.post(\n\t\"/\",\n\tjwtAuth,\n\tvValidator(\"json\", ArticleToCreate),\n\tasync (c) => {\n\t\tconst db = c.get(\"db\");\n\t\tconst self = c.get(\"jwtPayload\");\n\n\t\tconst { tagList, ...articlePayload } = c.req.valid(\"json\").article;\n\t\tconst slug = slugify(articlePayload.title);\n\n\t\tawait db.insert(articlesTable).values({\n\t\t\tslug,\n\t\t\t...articlePayload,\n\t\t\tauthorId: self.id,\n\t\t});\n\n\t\tif (tagList !== undefined) {\n\t\t\tawait db.insert(tagsTable).values(tagList.map((tag) => ({ tag })));\n\t\t\tawait db\n\t\t\t\t.insert(articleTagTable)\n\t\t\t\t.values(tagList.map((tag) => ({ articleSlug: slug, tag })));\n\t\t}\n\n\t\tconst article = await findArticle(db, slug, self);\n\n\t\treturn c.json(parse(SingleArticleResponse, { article }));\n\t},\n);\n\narticlesModule.put(\n\t\"/:slug\",\n\tjwtAuth,\n\tvValidator(\"json\", UpdatedArticle),\n\tasync (c) => {\n\t\tconst db = c.get(\"db\");\n\t\tconst self = c.get(\"jwtPayload\");\n\n\t\tlet slug = c.req.param(\"slug\");\n\t\tconst { article: articlePayload } = c.req.valid(\"json\");\n\n\t\tconst [articleOwnership] = await db\n\t\t\t.select({ isOwned: eq(articlesTable.authorId, self.id) })\n\t\t\t.from(articlesTable)\n\t\t\t.where(eq(articlesTable.slug, slug));\n\n\t\tif (articleOwnership === undefined) {\n\t\t\treturn c.notFound();\n\t\t}\n\t\tif (!articleOwnership.isOwned) {\n\t\t\treturn new Response(\"Forbidden\", { status: 403 });\n\t\t}\n\n\t\tawait db\n\t\t\t.update(articlesTable)\n\t\t\t.set(articlePayload)\n\t\t\t.where(eq(articlesTable.slug, slug));\n\n\t\tif (articlePayload.title !== undefined) {\n\t\t\tconst newSlug = slugify(articlePayload.title);\n\t\t\tawait db\n\t\t\t\t.update(articlesTable)\n\t\t\t\t.set({ slug: newSlug })\n\t\t\t\t.where(eq(articlesTable.slug, slug));\n\t\t\tslug = newSlug;\n\t\t}\n\n\t\tconst article = await findArticle(db, slug, self);\n\n\t\treturn c.json(parse(SingleArticleResponse, { article }));\n\t},\n);\n\narticlesModule.delete(\"/:slug\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\n\tconst slug = c.req.param(\"slug\");\n\n\tconst [articleOwnership] = await db\n\t\t.select({ isOwned: eq(articlesTable.authorId, self.id) })\n\t\t.from(articlesTable)\n\t\t.where(eq(articlesTable.slug, slug));\n\n\tif (articleOwnership === undefined) {\n\t\treturn c.notFound();\n\t}\n\tif (!articleOwnership.isOwned) {\n\t\treturn new Response(\"Forbidden\", { status: 403 });\n\t}\n\n\tawait db.delete(articlesTable).where(eq(articlesTable.slug, slug));\n\n\treturn new Response(null, { status: 204 });\n});\n\narticlesModule.post(\"/:slug/favorite\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\n\tconst slug = c.req.param(\"slug\");\n\n\tconst [articleExists] = await db\n\t\t.select({ exists: sql`1` })\n\t\t.from(articlesTable)\n\t\t.where(eq(articlesTable.slug, slug));\n\n\tif (articleExists === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\tawait db\n\t\t.insert(articleFavoriteTable)\n\t\t.values({\n\t\t\tarticleSlug: slug,\n\t\t\tuserId: self.id,\n\t\t})\n\t\t.onConflictDoNothing();\n\n\tconst updatedArticle = await findArticle(db, slug, self);\n\n\treturn c.json(parse(SingleArticleResponse, { article: updatedArticle }));\n});\n\narticlesModule.delete(\"/:slug/favorite\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\n\tconst slug = c.req.param(\"slug\");\n\n\tconst [articleExists] = await db\n\t\t.select({ exists: sql`1` })\n\t\t.from(articlesTable)\n\t\t.where(eq(articlesTable.slug, slug));\n\n\tif (articleExists === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\tawait db\n\t\t.delete(articleFavoriteTable)\n\t\t.where(\n\t\t\tand(\n\t\t\t\teq(articleFavoriteTable.articleSlug, slug),\n\t\t\t\teq(articleFavoriteTable.userId, self.id),\n\t\t\t),\n\t\t);\n\n\tconst updatedArticle = await findArticle(db, slug, self);\n\n\treturn c.json(parse(SingleArticleResponse, { article: updatedArticle }));\n});\n","import { createMiddleware } from \"hono/factory\";\nimport { jwt } from \"hono/jwt\";\nimport { type InferOutput, number, object } from \"valibot\";\n\nimport type { ThisAppEnv } from \"./factory.js\";\n\nexport const JwtClaims = object({\n\tid: number(),\n});\n\nexport const jwtAuth = createMiddleware<{\n\tVariables: { jwtPayload: InferOutput<typeof JwtClaims> };\n\tBindings: ThisAppEnv[\"Bindings\"];\n}>((c, next) => {\n\tconst jwtMiddleware = jwt({\n\t\tsecret: c.env.JWT_SECRET,\n\t});\n\treturn jwtMiddleware(c, next);\n});\n\nexport const exposeToken = createMiddleware<{\n\tVariables: {\n\t\ttoken: string | undefined;\n\t};\n}>(async (c, next) => {\n\tconst token = c.req.header(\"Authorization\")?.split(\" \")[1];\n\n\tc.set(\"token\", token);\n\tawait next();\n});\n","import { and, eq, exists, sql } from \"drizzle-orm\";\nimport type { LibSQLDatabase } from \"drizzle-orm/libsql\";\nimport type { SQLiteColumn } from \"drizzle-orm/sqlite-core\";\n\nimport type * as schema from \"../../db/schema.js\";\nimport { userFollowTable } from \"../../db/schema.js\";\n\n/** Subquery to include the `following` field on a user object. */\nexport function amIFollowing({\n\tdb,\n\tme,\n\tthem,\n}: { db: LibSQLDatabase<typeof schema>; them: SQLiteColumn; me: number }) {\n\treturn exists(\n\t\tdb\n\t\t\t.select({ exists: sql`1` })\n\t\t\t.from(userFollowTable)\n\t\t\t.where(\n\t\t\t\tand(\n\t\t\t\t\teq(userFollowTable.followerId, me),\n\t\t\t\t\teq(userFollowTable.followedId, them),\n\t\t\t\t),\n\t\t\t),\n\t);\n}\n","import {\n\tarray,\n\tboolean,\n\tnumber,\n\tobject,\n\tomit,\n\toptional,\n\tpartial,\n\tstring,\n} from \"valibot\";\nimport { Profile } from \"../profiles/schema.js\";\n\nconst Article = object({\n\tslug: string(),\n\ttitle: string(),\n\tdescription: string(),\n\tbody: string(),\n\ttagList: array(string()),\n\tcreatedAt: string(),\n\tupdatedAt: string(),\n\tfavorited: boolean(),\n\tfavoritesCount: number(),\n\tauthor: Profile,\n});\n\nconst Comment = object({\n\tid: number(),\n\tcreatedAt: string(),\n\tupdatedAt: string(),\n\tbody: string(),\n\tauthor: Profile,\n});\n\nexport const SingleArticleResponse = object({\n\tarticle: Article,\n});\n\nexport const MultipleArticlesResponse = object({\n\tarticles: array(omit(Article, [\"body\"])),\n\tarticlesCount: number(),\n});\n\nexport const ArticleToCreate = object({\n\tarticle: object({\n\t\ttitle: string(),\n\t\tdescription: string(),\n\t\tbody: string(),\n\t\ttagList: optional(array(string())),\n\t}),\n});\n\nexport const UpdatedArticle = object({\n\tarticle: partial(\n\t\tobject({\n\t\t\ttitle: string(),\n\t\t\tdescription: string(),\n\t\t\tbody: string(),\n\t\t}),\n\t),\n});\n\nexport const SingleCommentResponse = object({\n\tcomment: Comment,\n});\n\nexport const MultipleCommentsResponse = object({\n\tcomments: array(Comment),\n});\n\nexport const CommentToCreate = object({\n\tcomment: object({\n\t\tbody: string(),\n\t}),\n});\n","import { boolean, nullable, object, string } from \"valibot\";\n\nexport const Profile = object({\n\tusername: string(),\n\tbio: nullable(string()),\n\timage: nullable(string()),\n\tfollowing: boolean(),\n});\n\nexport const ProfileResponse = object({\n\tprofile: Profile,\n});\n","import { and, eq, getTableColumns, sql } from \"drizzle-orm\";\nimport { Hono } from \"hono\";\nimport { decode } from \"hono/jwt\";\nimport { parse } from \"valibot\";\n\nimport { vValidator } from \"@hono/valibot-validator\";\nimport { JwtClaims, exposeToken, jwtAuth } from \"../../auth.js\";\nimport { articlesTable, commentsTable, usersTable } from \"../../db/schema.js\";\nimport type { ThisAppEnv } from \"../../factory.js\";\nimport { amIFollowing } from \"./am-i-following.js\";\nimport {\n\tCommentToCreate,\n\tMultipleCommentsResponse,\n\tSingleCommentResponse,\n} from \"./schema.js\";\n\nexport const commentsModule = new Hono<ThisAppEnv>();\n\ncommentsModule.get(\"/:slug/comments\", exposeToken, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst token = c.get(\"token\");\n\tconst self =\n\t\ttoken !== undefined ? parse(JwtClaims, decode(token).payload) : null;\n\n\tconst slug = c.req.param(\"slug\");\n\n\tconst [articleExists] = await db\n\t\t.select({ exists: sql`1` })\n\t\t.from(articlesTable)\n\t\t.where(eq(articlesTable.slug, slug));\n\n\tif (!articleExists) {\n\t\treturn c.notFound();\n\t}\n\n\tconst {\n\t\tauthorId: _authorId,\n\t\tarticleSlug: _articleSlug,\n\t\t...desiredColumns\n\t} = getTableColumns(commentsTable);\n\tconst comments = await db\n\t\t.select({\n\t\t\t...desiredColumns,\n\t\t\tauthor: {\n\t\t\t\tusername: usersTable.username,\n\t\t\t\tbio: usersTable.bio,\n\t\t\t\timage: usersTable.image,\n\t\t\t\tfollowing: (self === null\n\t\t\t\t\t? sql<number>`0`\n\t\t\t\t\t: amIFollowing({ db, them: commentsTable.authorId, me: self.id })\n\t\t\t\t).mapWith(Boolean),\n\t\t\t},\n\t\t})\n\t\t.from(commentsTable)\n\t\t.innerJoin(usersTable, eq(commentsTable.authorId, usersTable.id))\n\t\t.where(eq(commentsTable.articleSlug, slug));\n\n\treturn c.json(parse(MultipleCommentsResponse, { comments }));\n});\n\ncommentsModule.post(\n\t\"/:slug/comments\",\n\tjwtAuth,\n\tvValidator(\"json\", CommentToCreate),\n\tasync (c) => {\n\t\tconst db = c.get(\"db\");\n\t\tconst self = c.get(\"jwtPayload\");\n\n\t\tconst slug = c.req.param(\"slug\");\n\n\t\tconst [articleExists] = await db\n\t\t\t.select({ exists: sql`1` })\n\t\t\t.from(articlesTable)\n\t\t\t.where(eq(articlesTable.slug, slug));\n\n\t\tif (!articleExists) {\n\t\t\treturn c.notFound();\n\t\t}\n\n\t\tconst {\n\t\t\tauthorId: _authorId,\n\t\t\tarticleSlug: _articleSlug,\n\t\t\t...desiredColumns\n\t\t} = getTableColumns(commentsTable);\n\t\tconst commentPayload = c.req.valid(\"json\").comment;\n\t\tconst [addedComment] = await db\n\t\t\t.insert(commentsTable)\n\t\t\t.values([{ ...commentPayload, articleSlug: slug, authorId: self.id }])\n\t\t\t.returning(desiredColumns);\n\n\t\tif (addedComment === undefined) {\n\t\t\tthrow new Error(\"Failed to insert a comment\");\n\t\t}\n\n\t\tconst selfProfile = await db.query.usersTable.findFirst({\n\t\t\tcolumns: {\n\t\t\t\tusername: true,\n\t\t\t\tbio: true,\n\t\t\t\timage: true,\n\t\t\t},\n\t\t\twhere: eq(usersTable.id, self.id),\n\t\t});\n\n\t\treturn c.json(\n\t\t\tparse(SingleCommentResponse, {\n\t\t\t\tcomment: {\n\t\t\t\t\t...addedComment,\n\t\t\t\t\tauthor: { ...selfProfile, following: false },\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t},\n);\n\ncommentsModule.delete(\"/:slug/comments/:id\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\n\tconst slug = c.req.param(\"slug\");\n\tconst id = Number.parseInt(c.req.param(\"id\"), 10);\n\n\tconst [commentOwnership] = await db\n\t\t.select({ isOwned: eq(commentsTable.authorId, self.id) })\n\t\t.from(commentsTable)\n\t\t.where(and(eq(commentsTable.id, id), eq(commentsTable.articleSlug, slug)));\n\n\tif (commentOwnership === undefined) {\n\t\treturn c.notFound();\n\t}\n\tif (!commentOwnership.isOwned) {\n\t\treturn new Response(\"Forbidden\", { status: 403 });\n\t}\n\n\tawait db.delete(commentsTable).where(eq(commentsTable.id, id));\n\n\treturn new Response(null, { status: 204 });\n});\n","import { and, eq } from \"drizzle-orm\";\nimport { Hono } from \"hono\";\nimport { parse } from \"valibot\";\n\nimport { decode } from \"hono/jwt\";\nimport { JwtClaims, exposeToken, jwtAuth } from \"../../auth.js\";\nimport { userFollowTable, usersTable } from \"../../db/schema.js\";\nimport type { ThisAppEnv } from \"../../factory.js\";\nimport { ProfileResponse } from \"./schema.js\";\n\nexport const profilesModule = new Hono<ThisAppEnv>();\n\nprofilesModule.get(\"/:username\", exposeToken, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst token = c.get(\"token\");\n\tconst self =\n\t\ttoken !== undefined ? parse(JwtClaims, decode(token).payload) : null;\n\n\tconst user = await db.query.usersTable.findFirst({\n\t\twhere: eq(usersTable.username, c.req.param(\"username\")),\n\t});\n\n\tif (user === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\tconst following =\n\t\tself !== null\n\t\t\t? (await db.query.userFollowTable.findFirst({\n\t\t\t\t\twhere: and(\n\t\t\t\t\t\teq(userFollowTable.followerId, self.id),\n\t\t\t\t\t\teq(userFollowTable.followedId, user.id),\n\t\t\t\t\t),\n\t\t\t\t})) !== undefined\n\t\t\t: false;\n\n\treturn c.json(parse(ProfileResponse, { profile: { ...user, following } }));\n});\n\nprofilesModule.post(\"/:username/follow\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\tconst userToFollow = await db.query.usersTable.findFirst({\n\t\twhere: eq(usersTable.username, c.req.param(\"username\")),\n\t});\n\n\tif (userToFollow === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\tawait db.insert(userFollowTable).values({\n\t\tfollowerId: self.id,\n\t\tfollowedId: userToFollow.id,\n\t});\n\n\treturn c.json(\n\t\tparse(ProfileResponse, { profile: { ...userToFollow, following: true } }),\n\t);\n});\n\nprofilesModule.delete(\"/:username/follow\", jwtAuth, async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\tconst userToUnfollow = await db.query.usersTable.findFirst({\n\t\twhere: eq(usersTable.username, c.req.param(\"username\")),\n\t});\n\n\tif (userToUnfollow === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\tawait db\n\t\t.delete(userFollowTable)\n\t\t.where(\n\t\t\tand(\n\t\t\t\teq(userFollowTable.followerId, self.id),\n\t\t\t\teq(userFollowTable.followedId, userToUnfollow.id),\n\t\t\t),\n\t\t);\n\n\treturn c.json(\n\t\tparse(ProfileResponse, {\n\t\t\tprofile: { ...userToUnfollow, following: false },\n\t\t}),\n\t);\n});\n","import { Hono } from \"hono\";\nimport { parse } from \"valibot\";\n\nimport type { ThisAppEnv } from \"../../factory.js\";\nimport { ListOfTags } from \"./schema.js\";\n\nexport const tagsModule = new Hono<ThisAppEnv>();\n\ntagsModule.get(\"/\", async (c) => {\n\tconst db = c.get(\"db\");\n\tconst tags = await db.query.tagsTable.findMany();\n\n\treturn c.json(parse(ListOfTags, { tags: tags.map(({ tag }) => tag) }));\n});\n","import { array, object, string } from \"valibot\";\n\nexport const ListOfTags = object({\n\ttags: array(string()),\n});\n","import { vValidator } from \"@hono/valibot-validator\";\nimport { eq } from \"drizzle-orm\";\nimport { Hono } from \"hono\";\nimport { parse } from \"valibot\";\n\nimport { exposeToken, jwtAuth } from \"../../auth.js\";\nimport { usersTable } from \"../../db/schema.js\";\nimport type { ThisAppEnv } from \"../../factory.js\";\nimport { UpdatedDetails, UserResponse } from \"./schema.js\";\n\nexport const userModule = new Hono<ThisAppEnv>().use(jwtAuth).use(exposeToken);\n\nuserModule.get(\"/\", async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\tconst user = await db.query.usersTable.findFirst({\n\t\twhere: eq(usersTable.id, self.id),\n\t});\n\n\tif (user === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\treturn c.json(\n\t\tparse(UserResponse, { user: { ...user, token: c.get(\"token\") } }),\n\t);\n});\n\nuserModule.put(\"/\", vValidator(\"json\", UpdatedDetails), async (c) => {\n\tconst db = c.get(\"db\");\n\tconst self = c.get(\"jwtPayload\");\n\tconst user = await db.query.usersTable.findFirst({\n\t\twhere: eq(usersTable.id, self.id),\n\t});\n\n\tif (user === undefined) {\n\t\treturn c.notFound();\n\t}\n\n\tconst requestData = c.req.valid(\"json\");\n\tconst [updatedUser] = await db\n\t\t.update(usersTable)\n\t\t.set(requestData.user)\n\t\t.where(eq(usersTable.id, self.id))\n\t\t.returning();\n\n\treturn c.json(\n\t\tparse(UserResponse, { user: { ...updatedUser, token: c.get(\"token\") } }),\n\t);\n});\n","import { url, email, nullable, object, partial, pipe, string } from \"valibot\";\n\nexport const LoginCredentials = object({\n\tuser: object({\n\t\temail: pipe(string(), email()),\n\t\tpassword: string(),\n\t}),\n});\n\nexport const RegistrationDetails = object({\n\tuser: object({\n\t\tusername: string(),\n\t\temail: pipe(string(), email()),\n\t\tpassword: string(),\n\t}),\n});\n\nexport const UpdatedDetails = object({\n\tuser: partial(\n\t\tobject({\n\t\t\tusername: string(),\n\t\t\temail: pipe(string(), email()),\n\t\t\tpassword: string(),\n\t\t\tbio: nullable(string()),\n\t\t\timage: nullable(pipe(string(), url())),\n\t\t}),\n\t),\n});\n\nexport const UserResponse = object({\n\tuser: object({\n\t\temail: string(),\n\t\ttoken: string(),\n\t\tusername: string(),\n\t\tbio: nullable(string()),\n\t\timage: nullable(string()),\n\t}),\n});\n","import { vValidator } from \"@hono/valibot-validator\";\nimport bcrypt from \"bcryptjs\";\nimport { eq, or } from \"drizzle-orm\";\nimport { Hono } from \"hono\";\nimport { sign } from \"hono/jwt\";\nimport { parse } from \"valibot\";\n\nimport { JwtClaims } from \"../../auth.js\";\nimport { usersTable } from \"../../db/schema.js\";\nimport type { ThisAppEnv } from \"../../factory.js\";\nimport {\n\tLoginCredentials,\n\tRegistrationDetails,\n\tUserResponse,\n} from \"./schema.js\";\n\nexport const usersModule = new Hono<ThisAppEnv>();\n\nusersModule.post(\"/login\", vValidator(\"json\", LoginCredentials), async (c) => {\n\tconst db = c.get(\"db\");\n\tconst requestData = c.req.valid(\"json\");\n\tconst user = await db.query.usersTable.findFirst({\n\t\twhere: eq(usersTable.email, requestData.user.email),\n\t});\n\n\tconst isMatch =\n\t\tuser !== undefined &&\n\t\t(await bcrypt.compare(requestData.user.password, user.passwordHash));\n\n\tif (!isMatch) {\n\t\treturn c.json({ errors: { password: [\"invalid for this email\"] } }, 422);\n\t}\n\n\tconst token = await sign(parse(JwtClaims, user), c.env.JWT_SECRET);\n\n\treturn c.json(parse(UserResponse, { user: { ...user, token } }));\n});\n\nusersModule.post(\"/\", vValidator(\"json\", RegistrationDetails), async (c) => {\n\tconst db = c.get(\"db\");\n\tconst requestData = c.req.valid(\"json\");\n\n\tconst existingUser = await db.query.usersTable.findFirst({\n\t\twhere: or(\n\t\t\teq(usersTable.email, requestData.user.email),\n\t\t\teq(usersTable.username, requestData.user.username),\n\t\t),\n\t});\n\n\tif (existingUser?.email === requestData.user.email) {\n\t\treturn c.json({ errors: { email: [\"already in use\"] } }, 422);\n\t}\n\tif (existingUser?.username === requestData.user.username) {\n\t\treturn c.json({ errors: { username: [\"already in use\"] } }, 422);\n\t}\n\n\tconst passwordHash = await bcrypt.hash(requestData.user.password, 10);\n\n\tconst [user] = await db\n\t\t.insert(usersTable)\n\t\t.values({\n\t\t\temail: requestData.user.email,\n\t\t\tusername: requestData.user.username,\n\t\t\tpasswordHash,\n\t\t})\n\t\t.returning();\n\n\tconst token = await sign(parse(JwtClaims, user), c.env.JWT_SECRET);\n\n\treturn c.json(parse(UserResponse, { user: { ...user, token } }));\n});\n"],"mappings":"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