diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 14f8e53..2301476 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -6,6 +6,7 @@ on:
pull_request:
branches:
- main
+ - drizzle-v1
jobs:
biome:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 76bac50..d31ea3c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -9,6 +9,7 @@ on:
push:
branches:
- main
+ - drizzle-v1
paths-ignore:
- 'docs/**'
workflow_dispatch:
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 1a47965..d7dce40 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -2,7 +2,7 @@
## Overview
-**Bedstack (Stripped)** is a _distilled version_ of [the full Bedstack architecture](https://github.com/bedtime-coders/bedstack/blob/main/ARCHITECTURE.md). It keeps the _feature-sliced, modular structure_ but simplifies the layering for _rapid prototyping_.
+**Bedstack (Stripped)** is a _distilled version_ of [the full Bedstack architecture](https://github.com/bedtime-coders/bedstack/blob/drizzle-v1/ARCHITECTURE.md). It keeps the _feature-sliced, modular structure_ but simplifies the layering for _rapid prototyping_.
Each feature is self-contained and designed for clarity, fast development, and maintainability - without the overhead of full enterprise layering.
@@ -109,7 +109,7 @@ drizzle/ # Migrations, reset, seed
### See Also
-- [Bedstack Full Architecture](https://github.com/bedtime-coders/bedstack/blob/main/ARCHITECTURE.md)
+- [Bedstack Full Architecture](https://github.com/bedtime-coders/bedstack/blob/drizzle-v1/ARCHITECTURE.md)
- [ElysiaJS Docs](https://elysiajs.com/docs)
- [Drizzle ORM Docs](https://orm.drizzle.team/docs)
- [TypeBox Docs](https://typebox.io/docs)
diff --git a/README.md b/README.md
index c7033a2..63c6833 100644
--- a/README.md
+++ b/README.md
@@ -3,12 +3,22 @@
Bedstack (Stripped)
-[](https://github.com/bedtime-coders/bedstac/actions/workflows/tests.yml?query=branch%3Amain+event%3Apush) [](https://discord.gg/8UcP9QB5AV) [](https://github.com/bedtime-coders/bedstack-stripped/blob/main/LICENSE) [](https://bun.sh/) [](https://elysiajs.com/) [](https://drizzle.team/) [](https://biomejs.dev/) [](https://scalar.com/) [](https://github.com/bedtime-coders/bedstack-stripped/stargazers/)
+[](https://github.com/bedtime-coders/bedstac/actions/workflows/tests.yml?query=branch%drizzle-v1+event%3Apush) [](https://discord.gg/8UcP9QB5AV) [](https://github.com/bedtime-coders/bedstack-stripped/blob/drizzle-v1/LICENSE) [](https://bun.sh/) [](https://elysiajs.com/) [](https://drizzle.team/) [](https://biomejs.dev/) [](https://scalar.com/) [](https://github.com/bedtime-coders/bedstack-stripped/stargazers/)
⚡ Stripped version of [Bedstack](https://github.com/bedtime-coders/bedstack) for rapid prototyping
+> [!IMPORTANT]
+> You are viewing the **`drizzle-v1`** branch of _Bedstack (Stripped)_, where we use [Drizzle ORM v1, currently in beta](https://orm.drizzle.team/roadmap). Click [here](https://github.com/bedtime-coders/bedstack-stripped/tree/main) to view the main branch.
+
+## Drizzle ORM v1 & Relational API v2
+
+This branch uses [Drizzle ORM v1](https://orm.drizzle.team/roadmap), which is currently in beta. The main feature from Drizzle ORM v1 that we use here is Relational API v2 (often referred to as rqbv2).
+
+- GitHub Discussion to learn more about Relational API v2: https://github.com/drizzle-team/drizzle-orm/discussions/2316.
+- Drizzle website page to track the release of Drizzle ORM v1: https://orm.drizzle.team/roadmap.
+
## Bedstack: Bun + ElysiaJS + Drizzle Stack
**Bedstack** is a collection of bleeding-edge technologies to build modern web applications.
diff --git a/scripts/db/reset.ts b/scripts/db/reset.ts
index c0a3e4c..cc610a6 100644
--- a/scripts/db/reset.ts
+++ b/scripts/db/reset.ts
@@ -1,22 +1,35 @@
+import { $ } from "bun";
import chalk from "chalk";
-import { drizzle } from "drizzle-orm/bun-sql";
-import { reset } from "drizzle-seed";
-import * as articlesSchema from "@/articles/articles.schema";
-import * as commentsSchema from "@/comments/comments.schema";
-import { env } from "@/core/env";
-import * as profilesSchema from "@/profiles/profiles.schema";
-import * as tagsSchema from "@/tags/tags.schema";
-import * as usersSchema from "@/users/users.schema";
-
-const schema = {
- ...usersSchema,
- ...profilesSchema,
- ...tagsSchema,
- ...articlesSchema,
- ...commentsSchema,
-};
+import { sql } from "drizzle-orm";
+import { db } from "@/core/database";
console.info(chalk.gray("Resetting database"));
-// See: https://github.com/drizzle-team/drizzle-orm/issues/3599
-await reset(drizzle(env.DATABASE_URL), schema);
+const query = sql`
+ -- Delete all tables
+ DO $$ DECLARE
+ r RECORD;
+ BEGIN
+ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = current_schema()) LOOP
+ EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
+ END LOOP;
+ END $$;
+
+ -- Delete enums
+ DO $$ DECLARE
+ r RECORD;
+ BEGIN
+ FOR r IN (select t.typname as enum_name
+ from pg_type t
+ join pg_enum e on t.oid = e.enumtypid
+ join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
+ where n.nspname = current_schema()) LOOP
+ EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.enum_name);
+ END LOOP;
+ END $$;
+ `;
+
+await db.execute(query);
+
+// Push the schema to the database
+await $`bun run db:push`.quiet();
console.info(`[${chalk.green("✓")}] Database reset complete`);
diff --git a/src/articles/articles.plugin.ts b/src/articles/articles.plugin.ts
index 4a10bd1..5ae6575 100644
--- a/src/articles/articles.plugin.ts
+++ b/src/articles/articles.plugin.ts
@@ -1,8 +1,7 @@
-import { and, eq, inArray } from "drizzle-orm";
+import { and, eq } from "drizzle-orm";
import { Elysia, NotFoundError, t } from "elysia";
import { StatusCodes } from "http-status-codes";
import { db } from "@/core/database/db";
-import { follows } from "@/profiles/profiles.schema";
import { DEFAULT_LIMIT, DEFAULT_OFFSET } from "@/shared/constants";
import { RealWorldError } from "@/shared/errors";
import { auth } from "@/shared/plugins";
@@ -29,86 +28,43 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
},
auth: { currentUserId },
}) => {
- const [authorUser, favoritedUser] = await Promise.all([
- authorUsername
- ? db.query.users.findFirst({
- // where: eq(users.username, authorUsername),
- // relational queries v2:
- where: {
- username: authorUsername,
- },
- })
- : undefined,
- favoritedByUsername
- ? db.query.users.findFirst({
- where: {
- username: favoritedByUsername,
- },
- })
- : undefined,
- ]);
-
- if (
- (authorUsername && !authorUser) ||
- (favoritedByUsername && !favoritedUser)
- ) {
- return toArticlesResponse([]);
- }
-
- // Build dynamic filters
- const filters = [];
- if (authorUser) {
- filters.push(eq(articles.authorId, authorUser.id));
- }
- if (favoritedUser) {
- filters.push(
- inArray(
- articles.id,
- db
- .select({ articleId: favorites.articleId })
- .from(favorites)
- .where(eq(favorites.userId, favoritedUser.id)),
- ),
- );
- }
- if (tagName) {
- filters.push(
- inArray(
- articles.id,
- db
- .select({ articleId: articlesToTags.articleId })
- .from(articlesToTags)
- .innerJoin(tags, eq(tags.id, articlesToTags.tagId))
- .where(eq(tags.name, tagName)),
- ),
- );
- }
-
const enrichedArticles = await db.query.articles.findMany({
where: {
- RAW: filters.length > 0 ? and(...filters) : undefined,
+ ...(authorUsername && {
+ author: {
+ username: authorUsername,
+ },
+ }),
+ ...(favoritedByUsername && {
+ favorites: {
+ user: {
+ username: favoritedByUsername,
+ },
+ },
+ }),
+ ...(tagName && {
+ tags: {
+ name: tagName,
+ },
+ }),
},
with: {
- author: currentUserId
- ? {
- with: {
- followers: {
- where: eq(follows.followerId, currentUserId),
- },
+ author: {
+ with: {
+ followers: {
+ where: {
+ id: currentUserId,
},
- }
- : true,
- tags: { with: { tag: true } },
+ },
+ },
+ },
+ tags: true,
favorites: true,
},
- // orderBy: [desc(articles.createdAt)],
- orderBy: {
- createdAt: "desc",
- },
+ orderBy: { createdAt: "desc" },
limit,
offset,
});
-
return toArticlesResponse(enrichedArticles, {
currentUserId,
});
@@ -129,16 +85,16 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
const enrichedArticle = await db.query.articles.findFirst({
where: { slug },
with: {
- author: currentUserId
- ? {
- with: {
- followers: {
- where: eq(follows.followerId, currentUserId),
- },
+ author: {
+ with: {
+ followers: {
+ where: {
+ id: currentUserId,
},
- }
- : true,
- tags: { with: { tag: true } },
+ },
+ },
+ },
+ tags: true,
},
});
@@ -170,27 +126,25 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
query: { limit = DEFAULT_LIMIT, offset = DEFAULT_OFFSET },
auth: { currentUserId },
}) => {
- // Get followed user IDs
- const followed = await db
- .select({ followedId: follows.followedId })
- .from(follows)
- .where(eq(follows.followerId, currentUserId));
-
- const followedIds = followed.map((f) => f.followedId);
- if (followedIds.length === 0) return toArticlesResponse([]);
-
- // Get articles from followed authors
const enrichedArticles = await db.query.articles.findMany({
- where: inArray(articles.authorId, followedIds),
+ where: {
+ author: {
+ followers: {
+ id: currentUserId,
+ },
+ },
+ },
with: {
author: {
with: {
followers: {
- where: eq(follows.followerId, currentUserId),
+ where: {
+ id: currentUserId,
+ },
},
},
},
- tags: { with: { tag: true } },
+ tags: true,
favorites: true,
},
orderBy: {
@@ -199,7 +153,6 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
limit,
offset,
});
-
return toArticlesResponse(enrichedArticles, { currentUserId });
},
{
@@ -225,7 +178,11 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
.onConflictDoNothing();
const relevantTags = await db.query.tags.findMany({
- where: inArray(tags.name, tagList),
+ where: {
+ name: {
+ in: tagList,
+ },
+ },
});
const [createdArticle] = await db
@@ -255,7 +212,7 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
where: { id: createdArticle.id },
with: {
author: true,
- tags: { with: { tag: true } },
+ tags: true,
},
});
@@ -333,7 +290,11 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
// Get all relevant tags in one query
const relevantTags = await db.query.tags.findMany({
- where: inArray(tags.name, article.tagList),
+ where: {
+ name: {
+ in: article.tagList,
+ },
+ },
});
// Connect tags to article
@@ -353,11 +314,13 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
author: {
with: {
followers: {
- where: eq(follows.followerId, currentUserId),
+ where: {
+ id: currentUserId,
+ },
},
},
},
- tags: { with: { tag: true } },
+ tags: true,
favorites: true, // Load all favorites to get count
},
});
@@ -427,11 +390,13 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
author: {
with: {
followers: {
- where: eq(follows.followerId, currentUserId),
+ where: {
+ id: currentUserId,
+ },
},
},
},
- tags: { with: { tag: true } },
+ tags: true,
favorites: true, // Load all favorites to get count
},
});
@@ -458,11 +423,13 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
author: {
with: {
followers: {
- where: eq(follows.followerId, currentUserId),
+ where: {
+ id: currentUserId,
+ },
},
},
},
- tags: { with: { tag: true } },
+ tags: true,
favorites: true,
},
});
@@ -492,11 +459,13 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
author: {
with: {
followers: {
- where: eq(follows.followerId, currentUserId),
+ where: {
+ id: currentUserId,
+ },
},
},
},
- tags: { with: { tag: true } },
+ tags: true,
favorites: true, // Load all favorites to get count
},
});
@@ -527,11 +496,13 @@ export const articlesPlugin = new Elysia({ tags: ["Articles"] })
author: {
with: {
followers: {
- where: eq(follows.followerId, currentUserId),
+ where: {
+ id: currentUserId,
+ },
},
},
},
- tags: { with: { tag: true } },
+ tags: true,
favorites: true,
},
});
diff --git a/src/articles/interfaces/enriched-article.interface.ts b/src/articles/interfaces/enriched-article.interface.ts
index 23b81fb..d73b095 100644
--- a/src/articles/interfaces/enriched-article.interface.ts
+++ b/src/articles/interfaces/enriched-article.interface.ts
@@ -1,6 +1,5 @@
import type { InferSelectModel } from "drizzle-orm";
-import type { follows } from "@/profiles/profiles.schema";
-import type { articlesToTags, tags } from "@/tags/tags.schema";
+import type { tags } from "@/tags/tags.schema";
import type { users } from "@/users/users.schema";
import type { articles, favorites } from "../articles.schema";
@@ -9,13 +8,9 @@ import type { articles, favorites } from "../articles.schema";
*/
export type EnrichedArticle = InferSelectModel & {
author: InferSelectModel & {
- followers?: Array>;
+ followers?: Array>;
};
- tags: Array<
- InferSelectModel & {
- tag: InferSelectModel;
- }
- >;
+ tags: Array>;
favorites?: Array>;
};
@@ -24,7 +19,7 @@ export type EnrichedArticle = InferSelectModel & {
*/
export type PersonalizedEnrichedArticle = EnrichedArticle & {
author: InferSelectModel & {
- followers?: Array>;
+ followers?: Array>;
};
favorites: Array>;
};
diff --git a/src/articles/mappers/to-articles-response.ts b/src/articles/mappers/to-articles-response.ts
index fd6e819..bb6812c 100644
--- a/src/articles/mappers/to-articles-response.ts
+++ b/src/articles/mappers/to-articles-response.ts
@@ -14,8 +14,7 @@ export function toArticlesResponse(
const myFavorites =
article.favorites?.filter((f) => f.userId === currentUserId) ?? [];
const myFollows =
- article.author.followers?.filter((f) => f.followerId === currentUserId) ??
- [];
+ article.author.followers?.filter((f) => f.id === currentUserId) ?? [];
const favoritesCount = article.favorites?.length ?? 0;
const isFavorited = myFavorites.length > 0;
const isFollowing = myFollows.length > 0;
@@ -24,7 +23,7 @@ export function toArticlesResponse(
title: article.title,
description: article.description,
tagList: article.tags
- .map((t) => t.tag.name)
+ .map((t) => t.name)
.sort((a, b) => a.localeCompare(b)),
createdAt: article.createdAt.toISOString(),
updatedAt: article.updatedAt.toISOString(),
diff --git a/src/articles/mappers/to-response.ts b/src/articles/mappers/to-response.ts
index b6e64a0..a3f384c 100644
--- a/src/articles/mappers/to-response.ts
+++ b/src/articles/mappers/to-response.ts
@@ -39,7 +39,7 @@ export function toResponse(
const favorited = article.favorites?.some((f) => f.userId === currentUserId);
const favoritesCount = article.favorites?.length ?? 0;
const following = article.author.followers?.some(
- (f) => f.followerId === currentUserId,
+ (f) => f.id === currentUserId,
);
return {
@@ -49,7 +49,7 @@ export function toResponse(
description: article.description,
body: article.body,
tagList: article.tags
- .map((t) => t.tag.name)
+ .map((t) => t.name)
.sort((a, b) => a.localeCompare(b)),
createdAt: article.createdAt.toISOString(),
updatedAt: article.updatedAt.toISOString(),
diff --git a/src/comments/comments.plugin.ts b/src/comments/comments.plugin.ts
index 6f36ceb..710edb8 100644
--- a/src/comments/comments.plugin.ts
+++ b/src/comments/comments.plugin.ts
@@ -1,9 +1,7 @@
import { eq } from "drizzle-orm";
import { Elysia, NotFoundError, t } from "elysia";
import { StatusCodes } from "http-status-codes";
-import { articles } from "@/articles/articles.schema";
import { db } from "@/core/database/db";
-import { follows } from "@/profiles/profiles.schema";
import { RealWorldError } from "@/shared/errors";
import { auth } from "@/shared/plugins";
import { commentsModel, UUID } from "./comments.model";
@@ -27,28 +25,19 @@ export const commentsPlugin = new Elysia({ tags: ["Comments"] })
const enrichedComments = await db.query.comments.findMany({
with: {
- author: currentUserId
- ? {
- with: {
- followers: {
- where: { followerId: currentUserId },
- },
+ author: {
+ with: {
+ followers: {
+ where: {
+ id: currentUserId,
},
- }
- : true,
+ },
+ },
+ },
},
where: { articleId: article.id },
orderBy: { createdAt: "desc" },
});
-
- const firstComment = enrichedComments[0];
- if (firstComment) {
- const followers = firstComment.author?.followers;
- enrichedComments.forEach((comment) => {
- comment.author.followers = followers;
- });
- }
-
return toCommentsResponse(enrichedComments, { currentUserId });
},
{
@@ -100,7 +89,9 @@ export const commentsPlugin = new Elysia({ tags: ["Comments"] })
author: {
with: {
followers: {
- where: { followerId: currentUserId },
+ where: {
+ id: currentUserId,
+ },
},
},
},
diff --git a/src/comments/interfaces/enriched-comment.interface.ts b/src/comments/interfaces/enriched-comment.interface.ts
index aca41ef..f9c1acc 100644
--- a/src/comments/interfaces/enriched-comment.interface.ts
+++ b/src/comments/interfaces/enriched-comment.interface.ts
@@ -1,10 +1,9 @@
import type { InferSelectModel } from "drizzle-orm";
-import type { follows } from "@/profiles/profiles.schema";
import type { users } from "@/users/users.schema";
import type { comments } from "../comments.schema";
export type EnrichedComment = InferSelectModel & {
author: InferSelectModel & {
- followers?: InferSelectModel[];
+ followers?: Array>;
};
};
diff --git a/src/comments/mappers/to-response.mapper.ts b/src/comments/mappers/to-response.mapper.ts
index 7365c2e..7b06f56 100644
--- a/src/comments/mappers/to-response.mapper.ts
+++ b/src/comments/mappers/to-response.mapper.ts
@@ -37,7 +37,7 @@ export function toCommentResponse(
following: Boolean(
currentUserId &&
enrichedComment.author.followers?.some(
- (f) => f.followerId === currentUserId,
+ (f) => f.id === currentUserId,
),
),
},
diff --git a/src/core/database/relations.ts b/src/core/database/relations.ts
index 0e0ccd7..324d2c2 100644
--- a/src/core/database/relations.ts
+++ b/src/core/database/relations.ts
@@ -15,52 +15,107 @@ const schema = {
favorites,
};
-export const relations = defineRelations(
- schema,
- ({
- one,
- many,
- articles,
- users,
- comments,
- articlesToTags,
- favorites,
- tags,
- }) => ({
- articles: {
- user: one.users({
- from: articles.authorId,
- to: users.id,
- alias: "articles_authorId_users_id",
- }),
- usersViaComments: many.users({
- from: articles.id.through(comments.articleId),
- to: users.id.through(comments.authorId),
- alias: "articles_id_users_id_via_comments",
- }),
- tags: many.tags({
- from: articles.id.through(articlesToTags.articleId),
- to: tags.id.through(articlesToTags.tagId),
- }),
- usersViaFavorites: many.users({
- from: articles.id.through(favorites.articleId),
- to: users.id.through(favorites.userId),
- alias: "articles_id_users_id_via_favorites",
- }),
- },
- users: {
- articlesAuthorId: many.articles({
- alias: "articles_authorId_users_id",
- }),
- articlesViaComments: many.articles({
- alias: "articles_id_users_id_via_comments",
- }),
- articlesViaFavorites: many.articles({
- alias: "articles_id_users_id_via_favorites",
- }),
- },
- tags: {
- articles: many.articles(),
- },
- }),
-);
+export const relations = defineRelations(schema, (r) => ({
+ // Articles relations
+ articles: {
+ author: r.one.users({
+ from: r.articles.authorId,
+ to: r.users.id,
+ optional: false,
+ }),
+ tags: r.many.tags({
+ from: r.articles.id.through(r.articlesToTags.articleId),
+ to: r.tags.id.through(r.articlesToTags.tagId),
+ }),
+ comments: r.many.comments({
+ from: r.articles.id,
+ to: r.comments.articleId,
+ }),
+ favorites: r.many.favorites({
+ from: r.articles.id,
+ to: r.favorites.articleId,
+ }),
+ },
+
+ // Users relations
+ users: {
+ // One-to-many: users can have many articles, comments, favorites
+ articles: r.many.articles({
+ from: r.users.id,
+ to: r.articles.authorId,
+ }),
+ comments: r.many.comments({
+ from: r.users.id,
+ to: r.comments.authorId,
+ }),
+ favorites: r.many.favorites({
+ from: r.users.id,
+ to: r.favorites.userId,
+ }),
+
+ // Many-to-many: users can follow many users and be followed by many users
+ following: r.many.users({
+ from: r.users.id.through(r.follows.followerId),
+ to: r.users.id.through(r.follows.followedId),
+ }),
+ followers: r.many.users({
+ from: r.users.id.through(r.follows.followedId),
+ to: r.users.id.through(r.follows.followerId),
+ }),
+ },
+
+ // Comments relations
+ comments: {
+ author: r.one.users({
+ from: r.comments.authorId,
+ to: r.users.id,
+ optional: false,
+ }),
+ article: r.one.articles({
+ from: r.comments.articleId,
+ to: r.articles.id,
+ }),
+ },
+
+ // Tags relations
+ tags: {
+ articles: r.many.articles({
+ from: r.tags.id.through(r.articlesToTags.tagId),
+ to: r.articles.id.through(r.articlesToTags.articleId),
+ }),
+ },
+
+ // Junction table relations (if needed for direct access)
+ follows: {
+ follower: r.one.users({
+ from: r.follows.followerId,
+ to: r.users.id,
+ }),
+ followed: r.one.users({
+ from: r.follows.followedId,
+ to: r.users.id,
+ }),
+ },
+
+ articlesToTags: {
+ article: r.one.articles({
+ from: r.articlesToTags.articleId,
+ to: r.articles.id,
+ }),
+ tag: r.one.tags({
+ from: r.articlesToTags.tagId,
+ to: r.tags.id,
+ }),
+ },
+
+ favorites: {
+ user: r.one.users({
+ from: r.favorites.userId,
+ to: r.users.id,
+ }),
+ article: r.one.articles({
+ from: r.favorites.articleId,
+ to: r.articles.id,
+ }),
+ },
+}));
diff --git a/src/core/plugins/errors.plugin.ts b/src/core/plugins/errors.plugin.ts
index e58bf49..d035a43 100644
--- a/src/core/plugins/errors.plugin.ts
+++ b/src/core/plugins/errors.plugin.ts
@@ -1,4 +1,4 @@
-import { DrizzleQueryError } from "drizzle-orm/errors";
+import { DrizzleError } from "drizzle-orm/errors";
import { type Elysia, NotFoundError, ValidationError } from "elysia";
import { pick } from "radashi";
import { DEFAULT_ERROR_MESSAGE } from "@/shared/constants";
@@ -29,7 +29,7 @@ export const errors = (app: Elysia) =>
}
// db errors
- if (error instanceof DrizzleQueryError) {
+ if (error instanceof DrizzleError) {
return formatDBError(error);
}
diff --git a/src/profiles/profiles.plugin.ts b/src/profiles/profiles.plugin.ts
index 4821e4d..1eb642b 100644
--- a/src/profiles/profiles.plugin.ts
+++ b/src/profiles/profiles.plugin.ts
@@ -4,7 +4,6 @@ import { StatusCodes } from "http-status-codes";
import { db } from "@/core/database/db";
import { RealWorldError } from "@/shared/errors";
import { auth } from "@/shared/plugins";
-import { users } from "@/users/users.schema";
import { toResponse } from "./mappers";
import { profilesModel } from "./profiles.model";
import { follows } from "./profiles.schema";
diff --git a/src/profiles/profiles.schema.ts b/src/profiles/profiles.schema.ts
index 4cea6bc..90adc27 100644
--- a/src/profiles/profiles.schema.ts
+++ b/src/profiles/profiles.schema.ts
@@ -1,6 +1,7 @@
import { sql } from "drizzle-orm";
import {
check,
+ index,
pgTable,
primaryKey,
timestamp,
@@ -8,15 +9,19 @@ import {
} from "drizzle-orm/pg-core";
import { users } from "@/users/users.schema";
+/**
+ * Junction table for many-to-many relationship between users (following/followers)
+ * This implements the users-to-users relationship through the follows table
+ */
export const follows = pgTable(
"follows",
{
followerId: uuid("follower_id")
.notNull()
- .references(() => users.id),
+ .references(() => users.id, { onDelete: "cascade" }),
followedId: uuid("followed_id")
.notNull()
- .references(() => users.id),
+ .references(() => users.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at")
.notNull()
@@ -24,7 +29,17 @@ export const follows = pgTable(
.$onUpdate(() => new Date()),
},
(table) => [
+ // Primary key on both foreign key columns
primaryKey({ columns: [table.followerId, table.followedId] }),
+ // Individual indexes for single-side queries
+ index("follows_follower_id_idx").on(table.followerId),
+ index("follows_followed_id_idx").on(table.followedId),
+ // Composite index for efficient many-to-many relationship queries
+ index("follows_follower_followed_idx").on(
+ table.followerId,
+ table.followedId,
+ ),
+ // Prevent self-following
check(
"unique_follower_following",
sql`${table.followerId} != ${table.followedId}`,
diff --git a/src/shared/errors/utils.ts b/src/shared/errors/utils.ts
index 58b5629..88ffad4 100644
--- a/src/shared/errors/utils.ts
+++ b/src/shared/errors/utils.ts
@@ -1,4 +1,4 @@
-import type { DrizzleQueryError } from "drizzle-orm/errors";
+import type { DrizzleError } from "drizzle-orm/errors";
import type { NotFoundError, ValidationError } from "elysia";
import { ConflictingFieldsError } from "./conflicting-fields";
@@ -72,7 +72,7 @@ export function formatNotFoundError(error: NotFoundError) {
};
}
-export function formatDBError(error: DrizzleQueryError) {
+export function formatDBError(error: DrizzleError) {
console.error(error);
return {
errors: {
diff --git a/src/shared/plugins/auth.plugin.ts b/src/shared/plugins/auth.plugin.ts
index 1e8d3d0..8e4e9d7 100644
--- a/src/shared/plugins/auth.plugin.ts
+++ b/src/shared/plugins/auth.plugin.ts
@@ -1,10 +1,8 @@
-import { eq } from "drizzle-orm";
import { Elysia, t } from "elysia";
import { StatusCodes } from "http-status-codes";
import { db } from "@/core/database/db";
import { env } from "@/core/env";
import { RealWorldError } from "@/shared/errors";
-import { users } from "@/users/users.schema";
import { name } from "../../../package.json";
import jwt from "./jwt.plugin";
import { token } from "./token.plugin";
@@ -39,7 +37,7 @@ export const auth = new Elysia()
iat: Math.floor(Date.now() / 1000),
})) satisfies SignFn,
jwtPayload: jwtPayload || null,
- currentUserId: jwtPayload ? jwtPayload.uid : null,
+ currentUserId: jwtPayload ? jwtPayload.uid : undefined,
},
};
})
@@ -64,7 +62,9 @@ export const auth = new Elysia()
// Check user exists in DB
const user = await db.query.users.findFirst({
- where: eq(users.id, currentUserId),
+ where: {
+ id: currentUserId,
+ },
});
if (!user) {
throw new RealWorldError(StatusCodes.UNAUTHORIZED, {