From de27278ab6dc74c855f439f4621bed830e52cc5c Mon Sep 17 00:00:00 2001 From: "M.Srinivsas shankar" Date: Mon, 9 Feb 2026 22:36:57 +0530 Subject: [PATCH 1/2] CRUD operations and api endpoints Created for users --- src/controllers/feed.controller.ts | 84 +++++++ src/controllers/follow.controller.ts | 146 ++++++++++++ src/controllers/hashtag.controller.ts | 111 +++++++++ src/controllers/like.controller.ts | 144 +++++++++++ src/controllers/post.controller.ts | 224 ++++++++++++++++++ src/controllers/user.controller.ts | 196 +++++++++++++++ src/entities/Follow.ts | 36 +++ src/entities/Hashtag.ts | 25 ++ src/entities/Like.ts | 37 +++ src/entities/Post.ts | 48 ++++ src/entities/User.ts | 16 ++ src/index.ts | 10 + .../1713427300000-CreatePostTable.ts | 73 ++++++ .../1713427400000-CreateHashtagTable.ts | 46 ++++ .../1713427500000-CreatePostHashtagsTable.ts | 66 ++++++ .../1713427600000-CreateLikeTable.ts | 103 ++++++++ .../1713427700000-CreateFollowTable.ts | 103 ++++++++ src/routes/feed.routes.ts | 7 + src/routes/follow.routes.ts | 19 ++ src/routes/hashtag.routes.ts | 17 ++ src/routes/like.routes.ts | 19 ++ src/routes/post.routes.ts | 19 ++ src/validations/follow.validation.ts | 25 ++ src/validations/hashtag.validation.ts | 20 ++ src/validations/like.validation.ts | 16 ++ src/validations/post.validation.ts | 36 +++ 26 files changed, 1646 insertions(+) create mode 100644 src/controllers/feed.controller.ts create mode 100644 src/controllers/follow.controller.ts create mode 100644 src/controllers/hashtag.controller.ts create mode 100644 src/controllers/like.controller.ts create mode 100644 src/controllers/post.controller.ts create mode 100644 src/entities/Follow.ts create mode 100644 src/entities/Hashtag.ts create mode 100644 src/entities/Like.ts create mode 100644 src/entities/Post.ts create mode 100644 src/migrations/1713427300000-CreatePostTable.ts create mode 100644 src/migrations/1713427400000-CreateHashtagTable.ts create mode 100644 src/migrations/1713427500000-CreatePostHashtagsTable.ts create mode 100644 src/migrations/1713427600000-CreateLikeTable.ts create mode 100644 src/migrations/1713427700000-CreateFollowTable.ts create mode 100644 src/routes/feed.routes.ts create mode 100644 src/routes/follow.routes.ts create mode 100644 src/routes/hashtag.routes.ts create mode 100644 src/routes/like.routes.ts create mode 100644 src/routes/post.routes.ts create mode 100644 src/validations/follow.validation.ts create mode 100644 src/validations/hashtag.validation.ts create mode 100644 src/validations/like.validation.ts create mode 100644 src/validations/post.validation.ts diff --git a/src/controllers/feed.controller.ts b/src/controllers/feed.controller.ts new file mode 100644 index 0000000..1cbb079 --- /dev/null +++ b/src/controllers/feed.controller.ts @@ -0,0 +1,84 @@ +import { Request, Response } from 'express'; +import { Post } from '../entities/Post'; +import { Follow } from '../entities/Follow'; +import { User } from '../entities/User'; +import { AppDataSource } from '../data-source'; +import { In } from 'typeorm'; + +export class FeedController { + private postRepository = AppDataSource.getRepository(Post); + private followRepository = AppDataSource.getRepository(Follow); + private userRepository = AppDataSource.getRepository(User); + + async getFeed(req: Request, res: Response) { + try { + const userId = parseInt(req.query.userId as string); + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + if (!userId) { + return res.status(400).json({ message: 'User ID is required' }); + } + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const following = await this.followRepository.find({ + where: { followerId: userId }, + select: ['followingId'], + }); + + const followingIds = following.map((f) => f.followingId); + + if (followingIds.length === 0) { + return res.json({ + data: [], + pagination: { + total: 0, + limit, + offset, + hasMore: false, + }, + message: 'You are not following anyone yet', + }); + } + + const [posts, total] = await this.postRepository.findAndCount({ + where: { userId: In(followingIds) }, + relations: ['user', 'hashtags', 'likes'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + const feedPosts = posts.map((post) => ({ + id: post.id, + content: post.content, + createdAt: post.createdAt, + updatedAt: post.updatedAt, + author: { + id: post.user.id, + firstName: post.user.firstName, + lastName: post.user.lastName, + email: post.user.email, + }, + hashtags: post.hashtags?.map((h) => h.name) || [], + likeCount: post.likes?.length || 0, + })); + + res.json({ + data: feedPosts, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching feed', error }); + } + } +} diff --git a/src/controllers/follow.controller.ts b/src/controllers/follow.controller.ts new file mode 100644 index 0000000..37acc6c --- /dev/null +++ b/src/controllers/follow.controller.ts @@ -0,0 +1,146 @@ +import { Request, Response } from 'express'; +import { Follow } from '../entities/Follow'; +import { User } from '../entities/User'; +import { AppDataSource } from '../data-source'; + +export class FollowController { + private followRepository = AppDataSource.getRepository(Follow); + private userRepository = AppDataSource.getRepository(User); + + async getAllFollows(req: Request, res: Response) { + try { + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [follows, total] = await this.followRepository.findAndCount({ + relations: ['follower', 'following'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + res.json({ + data: follows, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching follows', error }); + } + } + + async getFollowById(req: Request, res: Response) { + try { + const follow = await this.followRepository.findOne({ + where: { id: parseInt(req.params.id) }, + relations: ['follower', 'following'], + }); + + if (!follow) { + return res.status(404).json({ message: 'Follow not found' }); + } + + res.json(follow); + } catch (error) { + res.status(500).json({ message: 'Error fetching follow', error }); + } + } + + async getFollowing(req: Request, res: Response) { + try { + const userId = parseInt(req.params.userId); + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [follows, total] = await this.followRepository.findAndCount({ + where: { followerId: userId }, + relations: ['following'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + res.json({ + data: follows.map((f) => f.following), + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching following list', error }); + } + } + + async createFollow(req: Request, res: Response) { + try { + const { followerId, followingId } = req.body; + + if (followerId === followingId) { + return res.status(400).json({ message: 'Users cannot follow themselves' }); + } + + const follower = await this.userRepository.findOneBy({ id: followerId }); + if (!follower) { + return res.status(404).json({ message: 'Follower user not found' }); + } + + const following = await this.userRepository.findOneBy({ id: followingId }); + if (!following) { + return res.status(404).json({ message: 'User to follow not found' }); + } + + const existingFollow = await this.followRepository.findOne({ + where: { followerId, followingId }, + }); + + if (existingFollow) { + return res.status(409).json({ message: 'Already following this user', follow: existingFollow }); + } + + const follow = this.followRepository.create({ followerId, followingId }); + const result = await this.followRepository.save(follow); + + const savedFollow = await this.followRepository.findOne({ + where: { id: result.id }, + relations: ['follower', 'following'], + }); + + res.status(201).json(savedFollow); + } catch (error) { + res.status(500).json({ message: 'Error creating follow', error }); + } + } + + async deleteFollow(req: Request, res: Response) { + try { + const result = await this.followRepository.delete(parseInt(req.params.id)); + if (result.affected === 0) { + return res.status(404).json({ message: 'Follow not found' }); + } + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: 'Error deleting follow', error }); + } + } + + async unfollow(req: Request, res: Response) { + try { + const { followerId, followingId } = req.body; + + const result = await this.followRepository.delete({ followerId, followingId }); + if (result.affected === 0) { + return res.status(404).json({ message: 'Follow relationship not found' }); + } + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: 'Error unfollowing user', error }); + } + } +} diff --git a/src/controllers/hashtag.controller.ts b/src/controllers/hashtag.controller.ts new file mode 100644 index 0000000..961af11 --- /dev/null +++ b/src/controllers/hashtag.controller.ts @@ -0,0 +1,111 @@ +import { Request, Response } from 'express'; +import { Hashtag } from '../entities/Hashtag'; +import { AppDataSource } from '../data-source'; + +export class HashtagController { + private hashtagRepository = AppDataSource.getRepository(Hashtag); + + async getAllHashtags(req: Request, res: Response) { + try { + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [hashtags, total] = await this.hashtagRepository.findAndCount({ + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + res.json({ + data: hashtags, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching hashtags', error }); + } + } + + async getHashtagById(req: Request, res: Response) { + try { + const hashtag = await this.hashtagRepository.findOne({ + where: { id: parseInt(req.params.id) }, + relations: ['posts'], + }); + + if (!hashtag) { + return res.status(404).json({ message: 'Hashtag not found' }); + } + + res.json({ + ...hashtag, + postCount: hashtag.posts?.length || 0, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching hashtag', error }); + } + } + + async createHashtag(req: Request, res: Response) { + try { + const { name } = req.body; + const normalizedName = name.toLowerCase().trim(); + + const existingHashtag = await this.hashtagRepository.findOneBy({ name: normalizedName }); + if (existingHashtag) { + return res.status(409).json({ message: 'Hashtag already exists', hashtag: existingHashtag }); + } + + const hashtag = this.hashtagRepository.create({ name: normalizedName }); + const result = await this.hashtagRepository.save(hashtag); + + res.status(201).json(result); + } catch (error) { + res.status(500).json({ message: 'Error creating hashtag', error }); + } + } + + async updateHashtag(req: Request, res: Response) { + try { + const hashtagId = parseInt(req.params.id); + const { name } = req.body; + + const hashtag = await this.hashtagRepository.findOneBy({ id: hashtagId }); + if (!hashtag) { + return res.status(404).json({ message: 'Hashtag not found' }); + } + + if (name) { + const normalizedName = name.toLowerCase().trim(); + + const existingHashtag = await this.hashtagRepository.findOneBy({ name: normalizedName }); + if (existingHashtag && existingHashtag.id !== hashtagId) { + return res.status(409).json({ message: 'Hashtag with this name already exists' }); + } + + hashtag.name = normalizedName; + } + + const result = await this.hashtagRepository.save(hashtag); + res.json(result); + } catch (error) { + res.status(500).json({ message: 'Error updating hashtag', error }); + } + } + + async deleteHashtag(req: Request, res: Response) { + try { + const result = await this.hashtagRepository.delete(parseInt(req.params.id)); + if (result.affected === 0) { + return res.status(404).json({ message: 'Hashtag not found' }); + } + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: 'Error deleting hashtag', error }); + } + } +} diff --git a/src/controllers/like.controller.ts b/src/controllers/like.controller.ts new file mode 100644 index 0000000..17d9a00 --- /dev/null +++ b/src/controllers/like.controller.ts @@ -0,0 +1,144 @@ +import { Request, Response } from 'express'; +import { Like } from '../entities/Like'; +import { User } from '../entities/User'; +import { Post } from '../entities/Post'; +import { AppDataSource } from '../data-source'; + +export class LikeController { + private likeRepository = AppDataSource.getRepository(Like); + private userRepository = AppDataSource.getRepository(User); + private postRepository = AppDataSource.getRepository(Post); + + async getAllLikes(req: Request, res: Response) { + try { + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [likes, total] = await this.likeRepository.findAndCount({ + relations: ['user', 'post'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + res.json({ + data: likes, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching likes', error }); + } + } + + async getLikeById(req: Request, res: Response) { + try { + const like = await this.likeRepository.findOne({ + where: { id: parseInt(req.params.id) }, + relations: ['user', 'post'], + }); + + if (!like) { + return res.status(404).json({ message: 'Like not found' }); + } + + res.json(like); + } catch (error) { + res.status(500).json({ message: 'Error fetching like', error }); + } + } + + async getLikesForPost(req: Request, res: Response) { + try { + const postId = parseInt(req.params.postId); + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [likes, total] = await this.likeRepository.findAndCount({ + where: { postId }, + relations: ['user'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + res.json({ + data: likes, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching likes for post', error }); + } + } + + async createLike(req: Request, res: Response) { + try { + const { userId, postId } = req.body; + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const post = await this.postRepository.findOneBy({ id: postId }); + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + const existingLike = await this.likeRepository.findOne({ + where: { userId, postId }, + }); + + if (existingLike) { + return res.status(409).json({ message: 'User has already liked this post', like: existingLike }); + } + + const like = this.likeRepository.create({ userId, postId }); + const result = await this.likeRepository.save(like); + + const savedLike = await this.likeRepository.findOne({ + where: { id: result.id }, + relations: ['user', 'post'], + }); + + res.status(201).json(savedLike); + } catch (error) { + res.status(500).json({ message: 'Error creating like', error }); + } + } + + async deleteLike(req: Request, res: Response) { + try { + const result = await this.likeRepository.delete(parseInt(req.params.id)); + if (result.affected === 0) { + return res.status(404).json({ message: 'Like not found' }); + } + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: 'Error deleting like', error }); + } + } + + async unlikePost(req: Request, res: Response) { + try { + const { userId, postId } = req.body; + + const result = await this.likeRepository.delete({ userId, postId }); + if (result.affected === 0) { + return res.status(404).json({ message: 'Like not found' }); + } + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: 'Error removing like', error }); + } + } +} diff --git a/src/controllers/post.controller.ts b/src/controllers/post.controller.ts new file mode 100644 index 0000000..2f5b6d4 --- /dev/null +++ b/src/controllers/post.controller.ts @@ -0,0 +1,224 @@ +import { Request, Response } from 'express'; +import { Post } from '../entities/Post'; +import { User } from '../entities/User'; +import { Hashtag } from '../entities/Hashtag'; +import { AppDataSource } from '../data-source'; + +export class PostController { + private postRepository = AppDataSource.getRepository(Post); + private userRepository = AppDataSource.getRepository(User); + private hashtagRepository = AppDataSource.getRepository(Hashtag); + + async getAllPosts(req: Request, res: Response) { + try { + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const [posts, total] = await this.postRepository.findAndCount({ + relations: ['user', 'hashtags', 'likes'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + const postsWithLikeCount = posts.map((post) => ({ + ...post, + likeCount: post.likes?.length || 0, + likes: undefined, + })); + + res.json({ + data: postsWithLikeCount, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching posts', error }); + } + } + + async getPostById(req: Request, res: Response) { + try { + const post = await this.postRepository.findOne({ + where: { id: parseInt(req.params.id) }, + relations: ['user', 'hashtags', 'likes'], + }); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + res.json({ + ...post, + likeCount: post.likes?.length || 0, + likes: undefined, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching post', error }); + } + } + + async getPostsByHashtag(req: Request, res: Response) { + try { + const tag = req.params.tag.toLowerCase().trim(); + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const hashtag = await this.hashtagRepository.findOne({ + where: { name: tag }, + }); + + if (!hashtag) { + return res.status(404).json({ message: 'Hashtag not found' }); + } + + const queryBuilder = this.postRepository + .createQueryBuilder('post') + .leftJoinAndSelect('post.user', 'user') + .leftJoinAndSelect('post.hashtags', 'hashtags') + .leftJoinAndSelect('post.likes', 'likes') + .innerJoin('post.hashtags', 'ht', 'ht.id = :hashtagId', { hashtagId: hashtag.id }) + .orderBy('post.createdAt', 'DESC'); + + const total = await queryBuilder.getCount(); + + const posts = await queryBuilder.skip(offset).take(limit).getMany(); + + const formattedPosts = posts.map((post) => ({ + id: post.id, + content: post.content, + createdAt: post.createdAt, + updatedAt: post.updatedAt, + author: { + id: post.user.id, + firstName: post.user.firstName, + lastName: post.user.lastName, + email: post.user.email, + }, + hashtags: post.hashtags?.map((h) => h.name) || [], + likeCount: post.likes?.length || 0, + })); + + res.json({ + hashtag: tag, + data: formattedPosts, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching posts by hashtag', error }); + } + } + + async createPost(req: Request, res: Response) { + try { + const { content, userId, hashtags } = req.body; + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const post = this.postRepository.create({ + content, + userId, + }); + + if (hashtags && hashtags.length > 0) { + const hashtagEntities: Hashtag[] = []; + + for (const tagName of hashtags) { + const normalizedName = tagName.toLowerCase().trim(); + let hashtag = await this.hashtagRepository.findOneBy({ name: normalizedName }); + + if (!hashtag) { + hashtag = this.hashtagRepository.create({ name: normalizedName }); + await this.hashtagRepository.save(hashtag); + } + hashtagEntities.push(hashtag); + } + + post.hashtags = hashtagEntities; + } + + const result = await this.postRepository.save(post); + + const savedPost = await this.postRepository.findOne({ + where: { id: result.id }, + relations: ['user', 'hashtags'], + }); + + res.status(201).json(savedPost); + } catch (error) { + res.status(500).json({ message: 'Error creating post', error }); + } + } + + async updatePost(req: Request, res: Response) { + try { + const postId = parseInt(req.params.id); + const { content, hashtags } = req.body; + + const post = await this.postRepository.findOne({ + where: { id: postId }, + relations: ['hashtags'], + }); + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + if (content) { + post.content = content; + } + + if (hashtags !== undefined) { + const hashtagEntities: Hashtag[] = []; + + for (const tagName of hashtags) { + const normalizedName = tagName.toLowerCase().trim(); + let hashtag = await this.hashtagRepository.findOneBy({ name: normalizedName }); + + if (!hashtag) { + hashtag = this.hashtagRepository.create({ name: normalizedName }); + await this.hashtagRepository.save(hashtag); + } + hashtagEntities.push(hashtag); + } + + post.hashtags = hashtagEntities; + } + + const result = await this.postRepository.save(post); + + const updatedPost = await this.postRepository.findOne({ + where: { id: result.id }, + relations: ['user', 'hashtags'], + }); + + res.json(updatedPost); + } catch (error) { + res.status(500).json({ message: 'Error updating post', error }); + } + } + + async deletePost(req: Request, res: Response) { + try { + const result = await this.postRepository.delete(parseInt(req.params.id)); + if (result.affected === 0) { + return res.status(404).json({ message: 'Post not found' }); + } + res.status(204).send(); + } catch (error) { + res.status(500).json({ message: 'Error deleting post', error }); + } + } +} diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index fb84fba..3b289fc 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,9 +1,16 @@ import { Request, Response } from 'express'; import { User } from '../entities/User'; +import { Post } from '../entities/Post'; +import { Like } from '../entities/Like'; +import { Follow } from '../entities/Follow'; import { AppDataSource } from '../data-source'; +import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; export class UserController { private userRepository = AppDataSource.getRepository(User); + private postRepository = AppDataSource.getRepository(Post); + private likeRepository = AppDataSource.getRepository(Like); + private followRepository = AppDataSource.getRepository(Follow); async getAllUsers(req: Request, res: Response) { try { @@ -65,4 +72,193 @@ export class UserController { res.status(500).json({ message: 'Error deleting user', error }); } } + + async getFollowers(req: Request, res: Response) { + try { + const userId = parseInt(req.params.id); + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const [follows, total] = await this.followRepository.findAndCount({ + where: { followingId: userId }, + relations: ['follower'], + order: { createdAt: 'DESC' }, + take: limit, + skip: offset, + }); + + const followers = follows.map((f) => ({ + id: f.follower.id, + firstName: f.follower.firstName, + lastName: f.follower.lastName, + email: f.follower.email, + followedAt: f.createdAt, + })); + + res.json({ + user: { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + }, + followerCount: total, + data: followers, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching followers', error }); + } + } + + async getActivity(req: Request, res: Response) { + try { + const userId = parseInt(req.params.id); + const type = req.query.type as string | undefined; + const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined; + const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined; + const limit = parseInt(req.query.limit as string) || 10; + const offset = parseInt(req.query.offset as string) || 0; + + const user = await this.userRepository.findOneBy({ id: userId }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const dateFilter = this.buildDateFilter(startDate, endDate); + + const activities: Array<{ + type: 'post' | 'like' | 'follow'; + data: any; + createdAt: Date; + }> = []; + + if (!type || type === 'post') { + const posts = await this.postRepository.find({ + where: { + userId, + ...(dateFilter && { createdAt: dateFilter }), + }, + relations: ['hashtags'], + order: { createdAt: 'DESC' }, + }); + + posts.forEach((post) => { + activities.push({ + type: 'post', + data: { + id: post.id, + content: post.content, + hashtags: post.hashtags?.map((h) => h.name) || [], + }, + createdAt: post.createdAt, + }); + }); + } + + if (!type || type === 'like') { + const likes = await this.likeRepository.find({ + where: { + userId, + ...(dateFilter && { createdAt: dateFilter }), + }, + relations: ['post', 'post.user'], + order: { createdAt: 'DESC' }, + }); + + likes.forEach((like) => { + activities.push({ + type: 'like', + data: { + id: like.id, + post: { + id: like.post.id, + content: like.post.content, + author: { + id: like.post.user.id, + firstName: like.post.user.firstName, + lastName: like.post.user.lastName, + }, + }, + }, + createdAt: like.createdAt, + }); + }); + } + + if (!type || type === 'follow') { + const follows = await this.followRepository.find({ + where: { + followerId: userId, + ...(dateFilter && { createdAt: dateFilter }), + }, + relations: ['following'], + order: { createdAt: 'DESC' }, + }); + + follows.forEach((follow) => { + activities.push({ + type: 'follow', + data: { + id: follow.id, + following: { + id: follow.following.id, + firstName: follow.following.firstName, + lastName: follow.following.lastName, + email: follow.following.email, + }, + }, + createdAt: follow.createdAt, + }); + }); + } + + activities.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + + const total = activities.length; + const paginatedActivities = activities.slice(offset, offset + limit); + + res.json({ + user: { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + }, + data: paginatedActivities, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + filters: { + type: type || 'all', + startDate: startDate?.toISOString() || null, + endDate: endDate?.toISOString() || null, + }, + }); + } catch (error) { + res.status(500).json({ message: 'Error fetching user activity', error }); + } + } + + private buildDateFilter(startDate?: Date, endDate?: Date) { + if (startDate && endDate) { + return Between(startDate, endDate); + } else if (startDate) { + return MoreThanOrEqual(startDate); + } else if (endDate) { + return LessThanOrEqual(endDate); + } + return undefined; + } } diff --git a/src/entities/Follow.ts b/src/entities/Follow.ts new file mode 100644 index 0000000..651ca50 --- /dev/null +++ b/src/entities/Follow.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + Index, + Unique, +} from 'typeorm'; +import { User } from './User'; + +@Entity('follows') +@Unique(['followerId', 'followingId']) +@Index(['followerId', 'followingId']) +export class Follow { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column({ type: 'int' }) + followerId: number; + + @Column({ type: 'int' }) + followingId: number; + + @CreateDateColumn() + createdAt: Date; + + @ManyToOne(() => User, (user) => user.following, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'followerId' }) + follower: User; + + @ManyToOne(() => User, (user) => user.followers, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'followingId' }) + following: User; +} diff --git a/src/entities/Hashtag.ts b/src/entities/Hashtag.ts new file mode 100644 index 0000000..74938bd --- /dev/null +++ b/src/entities/Hashtag.ts @@ -0,0 +1,25 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToMany, + Index, +} from 'typeorm'; +import { Post } from './Post'; + +@Entity('hashtags') +export class Hashtag { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column({ type: 'varchar', length: 255, unique: true }) + @Index() + name: string; + + @CreateDateColumn() + createdAt: Date; + + @ManyToMany(() => Post, (post) => post.hashtags) + posts: Post[]; +} diff --git a/src/entities/Like.ts b/src/entities/Like.ts new file mode 100644 index 0000000..5c1286e --- /dev/null +++ b/src/entities/Like.ts @@ -0,0 +1,37 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, + Index, + Unique, +} from 'typeorm'; +import { User } from './User'; +import { Post } from './Post'; + +@Entity('likes') +@Unique(['userId', 'postId']) +@Index(['userId', 'postId']) +export class Like { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column({ type: 'int' }) + userId: number; + + @Column({ type: 'int' }) + postId: number; + + @CreateDateColumn() + createdAt: Date; + + @ManyToOne(() => User, (user) => user.likes, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; + + @ManyToOne(() => Post, (post) => post.likes, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'postId' }) + post: Post; +} diff --git a/src/entities/Post.ts b/src/entities/Post.ts new file mode 100644 index 0000000..aace160 --- /dev/null +++ b/src/entities/Post.ts @@ -0,0 +1,48 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + OneToMany, + ManyToMany, + JoinColumn, + JoinTable, +} from 'typeorm'; +import { User } from './User'; +import { Hashtag } from './Hashtag'; +import { Like } from './Like'; + +@Entity('posts') +export class Post { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column({ type: 'text' }) + content: string; + + @Column({ type: 'int' }) + userId: number; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; + + @ManyToMany(() => Hashtag, (hashtag) => hashtag.posts) + @JoinTable({ + name: 'post_hashtags', + joinColumn: { name: 'postId', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'hashtagId', referencedColumnName: 'id' }, + }) + hashtags: Hashtag[]; + + @OneToMany(() => Like, (like) => like.post) + likes: Like[]; +} diff --git a/src/entities/User.ts b/src/entities/User.ts index 169e678..d413e3d 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -4,7 +4,11 @@ import { Column, CreateDateColumn, UpdateDateColumn, + OneToMany, } from 'typeorm'; +import { Post } from './Post'; +import { Like } from './Like'; +import { Follow } from './Follow'; @Entity('users') export class User { @@ -25,4 +29,16 @@ export class User { @UpdateDateColumn() updatedAt: Date; + + @OneToMany(() => Post, (post) => post.user) + posts: Post[]; + + @OneToMany(() => Like, (like) => like.user) + likes: Like[]; + + @OneToMany(() => Follow, (follow) => follow.following) + followers: Follow[]; + + @OneToMany(() => Follow, (follow) => follow.follower) + following: Follow[]; } diff --git a/src/index.ts b/src/index.ts index 8563757..2c3e356 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ import express from 'express'; import dotenv from 'dotenv'; import { userRouter } from './routes/user.routes'; +import { postRouter } from './routes/post.routes'; +import { hashtagRouter } from './routes/hashtag.routes'; +import { likeRouter } from './routes/like.routes'; +import { followRouter } from './routes/follow.routes'; +import { feedRouter } from './routes/feed.routes'; import { AppDataSource } from './data-source'; dotenv.config(); @@ -21,6 +26,11 @@ app.get('/', (req, res) => { }); app.use('/api/users', userRouter); +app.use('/api/posts', postRouter); +app.use('/api/hashtags', hashtagRouter); +app.use('/api/likes', likeRouter); +app.use('/api/follows', followRouter); +app.use('/api/feed', feedRouter); const PORT = process.env.PORT || 3000; diff --git a/src/migrations/1713427300000-CreatePostTable.ts b/src/migrations/1713427300000-CreatePostTable.ts new file mode 100644 index 0000000..6928389 --- /dev/null +++ b/src/migrations/1713427300000-CreatePostTable.ts @@ -0,0 +1,73 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm'; + +export class CreatePostTable1713427300000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'posts', + columns: [ + { + name: 'id', + type: 'integer', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'content', + type: 'text', + isNullable: false, + }, + { + name: 'userId', + type: 'integer', + isNullable: false, + }, + { + name: 'createdAt', + type: 'datetime', + default: 'CURRENT_TIMESTAMP', + }, + { + name: 'updatedAt', + type: 'datetime', + default: 'CURRENT_TIMESTAMP', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'posts', + new TableForeignKey({ + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createIndex( + 'posts', + new TableIndex({ + name: 'IDX_POSTS_USER_ID', + columnNames: ['userId'], + }) + ); + + await queryRunner.createIndex( + 'posts', + new TableIndex({ + name: 'IDX_POSTS_CREATED_AT', + columnNames: ['createdAt'], + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropIndex('posts', 'IDX_POSTS_CREATED_AT'); + await queryRunner.dropIndex('posts', 'IDX_POSTS_USER_ID'); + await queryRunner.dropTable('posts'); + } +} diff --git a/src/migrations/1713427400000-CreateHashtagTable.ts b/src/migrations/1713427400000-CreateHashtagTable.ts new file mode 100644 index 0000000..2ae0053 --- /dev/null +++ b/src/migrations/1713427400000-CreateHashtagTable.ts @@ -0,0 +1,46 @@ +import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; + +export class CreateHashtagTable1713427400000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'hashtags', + columns: [ + { + name: 'id', + type: 'integer', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'name', + type: 'varchar', + length: '255', + isUnique: true, + isNullable: false, + }, + { + name: 'createdAt', + type: 'datetime', + default: 'CURRENT_TIMESTAMP', + }, + ], + }), + true + ); + + await queryRunner.createIndex( + 'hashtags', + new TableIndex({ + name: 'IDX_HASHTAGS_NAME', + columnNames: ['name'], + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropIndex('hashtags', 'IDX_HASHTAGS_NAME'); + await queryRunner.dropTable('hashtags'); + } +} diff --git a/src/migrations/1713427500000-CreatePostHashtagsTable.ts b/src/migrations/1713427500000-CreatePostHashtagsTable.ts new file mode 100644 index 0000000..d3f1c1a --- /dev/null +++ b/src/migrations/1713427500000-CreatePostHashtagsTable.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm'; + +export class CreatePostHashtagsTable1713427500000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'post_hashtags', + columns: [ + { + name: 'postId', + type: 'integer', + isPrimary: true, + }, + { + name: 'hashtagId', + type: 'integer', + isPrimary: true, + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'post_hashtags', + new TableForeignKey({ + columnNames: ['postId'], + referencedTableName: 'posts', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'post_hashtags', + new TableForeignKey({ + columnNames: ['hashtagId'], + referencedTableName: 'hashtags', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createIndex( + 'post_hashtags', + new TableIndex({ + name: 'IDX_POST_HASHTAGS_POST_ID', + columnNames: ['postId'], + }) + ); + + await queryRunner.createIndex( + 'post_hashtags', + new TableIndex({ + name: 'IDX_POST_HASHTAGS_HASHTAG_ID', + columnNames: ['hashtagId'], + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropIndex('post_hashtags', 'IDX_POST_HASHTAGS_HASHTAG_ID'); + await queryRunner.dropIndex('post_hashtags', 'IDX_POST_HASHTAGS_POST_ID'); + await queryRunner.dropTable('post_hashtags'); + } +} diff --git a/src/migrations/1713427600000-CreateLikeTable.ts b/src/migrations/1713427600000-CreateLikeTable.ts new file mode 100644 index 0000000..94db67a --- /dev/null +++ b/src/migrations/1713427600000-CreateLikeTable.ts @@ -0,0 +1,103 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, + TableIndex, + TableUnique, +} from 'typeorm'; + +export class CreateLikeTable1713427600000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'likes', + columns: [ + { + name: 'id', + type: 'integer', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'userId', + type: 'integer', + isNullable: false, + }, + { + name: 'postId', + type: 'integer', + isNullable: false, + }, + { + name: 'createdAt', + type: 'datetime', + default: 'CURRENT_TIMESTAMP', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'likes', + new TableForeignKey({ + columnNames: ['userId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'likes', + new TableForeignKey({ + columnNames: ['postId'], + referencedTableName: 'posts', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createUniqueConstraint( + 'likes', + new TableUnique({ + name: 'UQ_LIKES_USER_POST', + columnNames: ['userId', 'postId'], + }) + ); + + await queryRunner.createIndex( + 'likes', + new TableIndex({ + name: 'IDX_LIKES_USER_POST', + columnNames: ['userId', 'postId'], + }) + ); + + await queryRunner.createIndex( + 'likes', + new TableIndex({ + name: 'IDX_LIKES_POST_ID', + columnNames: ['postId'], + }) + ); + + await queryRunner.createIndex( + 'likes', + new TableIndex({ + name: 'IDX_LIKES_USER_ID', + columnNames: ['userId'], + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropIndex('likes', 'IDX_LIKES_USER_ID'); + await queryRunner.dropIndex('likes', 'IDX_LIKES_POST_ID'); + await queryRunner.dropIndex('likes', 'IDX_LIKES_USER_POST'); + await queryRunner.dropUniqueConstraint('likes', 'UQ_LIKES_USER_POST'); + await queryRunner.dropTable('likes'); + } +} diff --git a/src/migrations/1713427700000-CreateFollowTable.ts b/src/migrations/1713427700000-CreateFollowTable.ts new file mode 100644 index 0000000..49067f9 --- /dev/null +++ b/src/migrations/1713427700000-CreateFollowTable.ts @@ -0,0 +1,103 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, + TableIndex, + TableUnique, +} from 'typeorm'; + +export class CreateFollowTable1713427700000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'follows', + columns: [ + { + name: 'id', + type: 'integer', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'followerId', + type: 'integer', + isNullable: false, + }, + { + name: 'followingId', + type: 'integer', + isNullable: false, + }, + { + name: 'createdAt', + type: 'datetime', + default: 'CURRENT_TIMESTAMP', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'follows', + new TableForeignKey({ + columnNames: ['followerId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'follows', + new TableForeignKey({ + columnNames: ['followingId'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createUniqueConstraint( + 'follows', + new TableUnique({ + name: 'UQ_FOLLOWS_FOLLOWER_FOLLOWING', + columnNames: ['followerId', 'followingId'], + }) + ); + + await queryRunner.createIndex( + 'follows', + new TableIndex({ + name: 'IDX_FOLLOWS_FOLLOWER_FOLLOWING', + columnNames: ['followerId', 'followingId'], + }) + ); + + await queryRunner.createIndex( + 'follows', + new TableIndex({ + name: 'IDX_FOLLOWS_FOLLOWER_ID', + columnNames: ['followerId'], + }) + ); + + await queryRunner.createIndex( + 'follows', + new TableIndex({ + name: 'IDX_FOLLOWS_FOLLOWING_ID', + columnNames: ['followingId'], + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropIndex('follows', 'IDX_FOLLOWS_FOLLOWING_ID'); + await queryRunner.dropIndex('follows', 'IDX_FOLLOWS_FOLLOWER_ID'); + await queryRunner.dropIndex('follows', 'IDX_FOLLOWS_FOLLOWER_FOLLOWING'); + await queryRunner.dropUniqueConstraint('follows', 'UQ_FOLLOWS_FOLLOWER_FOLLOWING'); + await queryRunner.dropTable('follows'); + } +} diff --git a/src/routes/feed.routes.ts b/src/routes/feed.routes.ts new file mode 100644 index 0000000..bac857a --- /dev/null +++ b/src/routes/feed.routes.ts @@ -0,0 +1,7 @@ +import { Router } from 'express'; +import { FeedController } from '../controllers/feed.controller'; + +export const feedRouter = Router(); +const feedController = new FeedController(); + +feedRouter.get('/', feedController.getFeed.bind(feedController)); diff --git a/src/routes/follow.routes.ts b/src/routes/follow.routes.ts new file mode 100644 index 0000000..bc0e0e6 --- /dev/null +++ b/src/routes/follow.routes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { validate } from '../middleware/validation.middleware'; +import { createFollowSchema } from '../validations/follow.validation'; +import { FollowController } from '../controllers/follow.controller'; + +export const followRouter = Router(); +const followController = new FollowController(); + +followRouter.get('/', followController.getAllFollows.bind(followController)); + +followRouter.get('/user/:userId/following', followController.getFollowing.bind(followController)); + +followRouter.get('/:id', followController.getFollowById.bind(followController)); + +followRouter.post('/', validate(createFollowSchema), followController.createFollow.bind(followController)); + +followRouter.delete('/:id', followController.deleteFollow.bind(followController)); + +followRouter.post('/unfollow', validate(createFollowSchema), followController.unfollow.bind(followController)); diff --git a/src/routes/hashtag.routes.ts b/src/routes/hashtag.routes.ts new file mode 100644 index 0000000..d914f95 --- /dev/null +++ b/src/routes/hashtag.routes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { validate } from '../middleware/validation.middleware'; +import { createHashtagSchema, updateHashtagSchema } from '../validations/hashtag.validation'; +import { HashtagController } from '../controllers/hashtag.controller'; + +export const hashtagRouter = Router(); +const hashtagController = new HashtagController(); + +hashtagRouter.get('/', hashtagController.getAllHashtags.bind(hashtagController)); + +hashtagRouter.get('/:id', hashtagController.getHashtagById.bind(hashtagController)); + +hashtagRouter.post('/', validate(createHashtagSchema), hashtagController.createHashtag.bind(hashtagController)); + +hashtagRouter.put('/:id', validate(updateHashtagSchema), hashtagController.updateHashtag.bind(hashtagController)); + +hashtagRouter.delete('/:id', hashtagController.deleteHashtag.bind(hashtagController)); diff --git a/src/routes/like.routes.ts b/src/routes/like.routes.ts new file mode 100644 index 0000000..480f3c2 --- /dev/null +++ b/src/routes/like.routes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { validate } from '../middleware/validation.middleware'; +import { createLikeSchema } from '../validations/like.validation'; +import { LikeController } from '../controllers/like.controller'; + +export const likeRouter = Router(); +const likeController = new LikeController(); + +likeRouter.get('/', likeController.getAllLikes.bind(likeController)); + +likeRouter.get('/post/:postId', likeController.getLikesForPost.bind(likeController)); + +likeRouter.get('/:id', likeController.getLikeById.bind(likeController)); + +likeRouter.post('/', validate(createLikeSchema), likeController.createLike.bind(likeController)); + +likeRouter.delete('/:id', likeController.deleteLike.bind(likeController)); + +likeRouter.post('/unlike', validate(createLikeSchema), likeController.unlikePost.bind(likeController)); diff --git a/src/routes/post.routes.ts b/src/routes/post.routes.ts new file mode 100644 index 0000000..24ac030 --- /dev/null +++ b/src/routes/post.routes.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { validate } from '../middleware/validation.middleware'; +import { createPostSchema, updatePostSchema } from '../validations/post.validation'; +import { PostController } from '../controllers/post.controller'; + +export const postRouter = Router(); +const postController = new PostController(); + +postRouter.get('/', postController.getAllPosts.bind(postController)); + +postRouter.get('/hashtag/:tag', postController.getPostsByHashtag.bind(postController)); + +postRouter.get('/:id', postController.getPostById.bind(postController)); + +postRouter.post('/', validate(createPostSchema), postController.createPost.bind(postController)); + +postRouter.put('/:id', validate(updatePostSchema), postController.updatePost.bind(postController)); + +postRouter.delete('/:id', postController.deletePost.bind(postController)); diff --git a/src/validations/follow.validation.ts b/src/validations/follow.validation.ts new file mode 100644 index 0000000..a78f8bc --- /dev/null +++ b/src/validations/follow.validation.ts @@ -0,0 +1,25 @@ +import Joi from 'joi'; + +export const createFollowSchema = Joi.object({ + followerId: Joi.number().required().integer().positive().messages({ + 'number.base': 'Follower ID must be a number', + 'number.integer': 'Follower ID must be an integer', + 'number.positive': 'Follower ID must be a positive number', + 'any.required': 'Follower ID is required', + }), + followingId: Joi.number().required().integer().positive().messages({ + 'number.base': 'Following ID must be a number', + 'number.integer': 'Following ID must be an integer', + 'number.positive': 'Following ID must be a positive number', + 'any.required': 'Following ID is required', + }), +}) + .custom((value, helpers) => { + if (value.followerId === value.followingId) { + return helpers.error('any.invalid', { message: 'Users cannot follow themselves' }); + } + return value; + }) + .messages({ + 'any.invalid': 'Users cannot follow themselves', + }); diff --git a/src/validations/hashtag.validation.ts b/src/validations/hashtag.validation.ts new file mode 100644 index 0000000..db0cdab --- /dev/null +++ b/src/validations/hashtag.validation.ts @@ -0,0 +1,20 @@ +import Joi from 'joi'; + +export const createHashtagSchema = Joi.object({ + name: Joi.string().required().min(1).max(100).messages({ + 'string.empty': 'Hashtag name is required', + 'string.min': 'Hashtag name must be at least 1 character long', + 'string.max': 'Hashtag name cannot exceed 100 characters', + }), +}); + +export const updateHashtagSchema = Joi.object({ + name: Joi.string().min(1).max(100).messages({ + 'string.min': 'Hashtag name must be at least 1 character long', + 'string.max': 'Hashtag name cannot exceed 100 characters', + }), +}) + .min(1) + .messages({ + 'object.min': 'At least one field must be provided for update', + }); diff --git a/src/validations/like.validation.ts b/src/validations/like.validation.ts new file mode 100644 index 0000000..b91bd80 --- /dev/null +++ b/src/validations/like.validation.ts @@ -0,0 +1,16 @@ +import Joi from 'joi'; + +export const createLikeSchema = Joi.object({ + userId: Joi.number().required().integer().positive().messages({ + 'number.base': 'User ID must be a number', + 'number.integer': 'User ID must be an integer', + 'number.positive': 'User ID must be a positive number', + 'any.required': 'User ID is required', + }), + postId: Joi.number().required().integer().positive().messages({ + 'number.base': 'Post ID must be a number', + 'number.integer': 'Post ID must be an integer', + 'number.positive': 'Post ID must be a positive number', + 'any.required': 'Post ID is required', + }), +}); diff --git a/src/validations/post.validation.ts b/src/validations/post.validation.ts new file mode 100644 index 0000000..22a6204 --- /dev/null +++ b/src/validations/post.validation.ts @@ -0,0 +1,36 @@ +import Joi from 'joi'; + +export const createPostSchema = Joi.object({ + content: Joi.string().required().min(1).max(5000).messages({ + 'string.empty': 'Content is required', + 'string.min': 'Content must be at least 1 character long', + 'string.max': 'Content cannot exceed 5000 characters', + }), + userId: Joi.number().required().integer().positive().messages({ + 'number.base': 'User ID must be a number', + 'number.integer': 'User ID must be an integer', + 'number.positive': 'User ID must be a positive number', + 'any.required': 'User ID is required', + }), + hashtags: Joi.array().items(Joi.string().min(1).max(100)).optional().messages({ + 'array.base': 'Hashtags must be an array', + 'string.min': 'Hashtag must be at least 1 character long', + 'string.max': 'Hashtag cannot exceed 100 characters', + }), +}); + +export const updatePostSchema = Joi.object({ + content: Joi.string().min(1).max(5000).messages({ + 'string.min': 'Content must be at least 1 character long', + 'string.max': 'Content cannot exceed 5000 characters', + }), + hashtags: Joi.array().items(Joi.string().min(1).max(100)).optional().messages({ + 'array.base': 'Hashtags must be an array', + 'string.min': 'Hashtag must be at least 1 character long', + 'string.max': 'Hashtag cannot exceed 100 characters', + }), +}) + .min(1) + .messages({ + 'object.min': 'At least one field must be provided for update', + }); From cd143dcd978373fe703f90fbde20c89fa804fdaf Mon Sep 17 00:00:00 2001 From: "M.Srinivsas shankar" Date: Mon, 9 Feb 2026 23:20:16 +0530 Subject: [PATCH 2/2] test script updated --- src/routes/user.routes.ts | 6 + test.sh | 451 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 443 insertions(+), 14 deletions(-) diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts index b28f626..4146244 100644 --- a/src/routes/user.routes.ts +++ b/src/routes/user.routes.ts @@ -9,6 +9,12 @@ const userController = new UserController(); // Get all users userRouter.get('/', userController.getAllUsers.bind(userController)); +// Get user followers +userRouter.get('/:id/followers', userController.getFollowers.bind(userController)); + +// Get user activity +userRouter.get('/:id/activity', userController.getActivity.bind(userController)); + // Get user by id userRouter.get('/:id', userController.getUserById.bind(userController)); diff --git a/test.sh b/test.sh index 16e8f0b..a3a62c1 100755 --- a/test.sh +++ b/test.sh @@ -2,6 +2,11 @@ # Base URLs USERS_URL="http://localhost:3000/api/users" +POSTS_URL="http://localhost:3000/api/posts" +HASHTAGS_URL="http://localhost:3000/api/hashtags" +LIKES_URL="http://localhost:3000/api/likes" +FOLLOWS_URL="http://localhost:3000/api/follows" +FEED_URL="http://localhost:3000/api/feed" # Colors for output GREEN='\033[0;32m' @@ -18,21 +23,26 @@ make_request() { local method=$1 local endpoint=$2 local data=$3 - + echo "Request: $method $endpoint" if [ -n "$data" ]; then echo "Data: $data" fi - + if [ "$method" = "GET" ]; then curl -s -X $method "$endpoint" | jq . + elif [ "$method" = "DELETE" ]; then + local http_code=$(curl -s -o /dev/null -w "%{http_code}" -X $method "$endpoint") + echo "Response status: $http_code" else curl -s -X $method "$endpoint" -H "Content-Type: application/json" -d "$data" | jq . fi echo "" } -# User-related functions +# ======================== +# User test functions +# ======================== test_get_all_users() { print_header "Testing GET all users" make_request "GET" "$USERS_URL" @@ -49,7 +59,7 @@ test_create_user() { read -p "Enter first name: " firstName read -p "Enter last name: " lastName read -p "Enter email: " email - + local user_data=$(cat <