Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions HANDOFF_COMMUNITY_BACKEND.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Handoff — Community backend PR

All files are in your working tree. To open a clean PR:

```bash
cd /Users/thorancherukuru/Nutri

# Get on a fresh branch off the latest master
git fetch origin
git checkout master
git pull
git checkout -b feat/community-backend

# Stage the new + modified files
git add migrations/create_community_tables.sql \
controller/communityController.js \
controller/supportData/communitySeed.js \
services/communityService.js \
validators/communityValidator.js \
routes/community.js \
routes/index.js \
test/community.controller.test.js \
docs/COMMUNITY_BACKEND.md \
HANDOFF_COMMUNITY_BACKEND.md

git status # confirm

# Commit
git commit -m "feat(community): backend for feed, posts, comments, likes, leaderboard

- 7 new endpoints under /api/community
- DB-first with bundled seed + in-memory fallback so frontend never blocks
- Standardized response envelope (matches support PR)
- 17 hermetic Jest tests
- Optional migration: migrations/create_community_tables.sql

Backs frontend tasks M-60 through M-63 (FeedScreen, PostDetailScreen,
CreatePostScreen, LeaderboardScreen)."

# Push the branch (NOT to master)
git push -u origin feat/community-backend
```

Then on GitHub:

1. Open a new PR from `thoran123:feat/community-backend` → `Gopher-Industries:master`.
2. Title: `BE30 : feat(community): feed, posts, comments, likes, leaderboard`
3. Description: paste the contents of `docs/COMMUNITY_BACKEND.md`.
4. Request review from Tien and Vedant.

## Important — don't push to master directly this time

Last time you ran `git push origin HEAD:master` which bypassed PR
review. The command above (`git push -u origin feat/community-backend`)
pushes the **branch** so GitHub's PR flow handles the merge.

## Verify locally before pushing

```bash
npm install
npx jest test/community.controller.test.js \
test/contactus.controller.test.js \
test/userFeedback.controller.test.js \
test/faq.controller.test.js \
test/healthTools.controller.test.js
# expected: 32 passing
```

## Tell the frontend team

Send Tien (or whoever owns the frontend tasks) this:

> Backend for community screens (M-60 → M-63) is in PR #XXX. Endpoints
> live under `/api/community`. Image upload is the existing
> `POST /api/upload` (returns `fileUrl`). Endpoints work today against
> bundled seed data, so you can build against them before the
> Supabase migration runs. See `docs/COMMUNITY_BACKEND.md` for the full
> contract and frontend-screen mapping.
8 changes: 8 additions & 0 deletions POST_MERGE_FIXES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Post-merge fixes — review feedback round 1
# Post-merge fixes — review feedback (round 1 + round 2)

The reviewer flagged three classes of issue after merging `feat/support-consolidation` into a local copy of `master`. None of them were merge conflicts — they were latent issues in the merged code/test setup. All three are fixed in this update.
Expand Down Expand Up @@ -51,6 +52,13 @@ FAIL security/test.js — Your test suite must contain at least one test

**Fix:** added `/security/` to `testPathIgnorePatterns` in `jest.config.js`. The script remains usable as a CLI tool (`node security/test.js`) but Jest no longer tries to run it.

## Net diff this round

```
modified: services/aiExecutionService.js (-18 lines, removed duplicate stub)
modified: jest.config.js (+25 lines, ignore patterns + integration toggle)
modified: package.json (+1 line, test:integration script)
new: POST_MERGE_FIXES.md (this file)
---

# Round 2 — additional reviewer feedback
Expand Down
218 changes: 218 additions & 0 deletions controller/communityController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* controller/communityController.js
*
* HTTP layer for community endpoints. Uses the standardized support
* response envelope (utils/supportResponse) so the frontend gets a
* consistent shape across the support + community surface.
*/

const { validationResult } = require('express-validator');
const support = require('../utils/supportResponse');
const logger = require('../utils/logger');
const community = require('../services/communityService');

function _userFrom(req) {
return req.user || {};
}

// ============================================================
// GET /api/community/posts
// ============================================================
async function listFeed(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

try {
const result = await community.listPosts({
page: req.query.page,
pageSize: req.query.pageSize,
});
return support.sendSuccess(
res,
{
items: result.items,
page: result.page,
pageSize: result.pageSize,
totalCount: result.totalCount,
hasMore: result.hasMore,
},
{ meta: { source: result.source, generatedAt: new Date().toISOString() } }
);
} catch (error) {
logger.error('communityController.listFeed failed', { error: error.message });
return support.sendError(res, 500, 'Unable to load feed right now.', 'COMMUNITY_FEED_FAILED');
}
}

// ============================================================
// GET /api/community/posts/:postId
// ============================================================
async function getPost(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

try {
const { post, source } = await community.getPost(req.params.postId);
if (!post) {
return support.sendError(res, 404, 'Post not found.', 'COMMUNITY_POST_NOT_FOUND');
}
return support.sendSuccess(res, { post }, { meta: { source } });
} catch (error) {
logger.error('communityController.getPost failed', { error: error.message });
return support.sendError(res, 500, 'Unable to load post.', 'COMMUNITY_POST_FAILED');
}
}

// ============================================================
// POST /api/community/posts (auth)
// ============================================================
async function createPost(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

const user = _userFrom(req);
if (!user.userId) {
return support.sendError(res, 401, 'Authentication required.', 'AUTH_REQUIRED');
}

try {
const { post, persistedTo } = await community.createPost({
userId: user.userId,
userName: user.name || user.email,
content: req.body.content,
imageUrl: req.body.imageUrl,
});
return support.sendCreated(
res,
{ post },
{ meta: { persistedTo, message: 'Post created.' } }
);
} catch (error) {
if (error.statusCode === 400) {
return support.sendError(res, 400, error.message, error.code || 'VALIDATION_ERROR');
}
logger.error('communityController.createPost failed', { error: error.message });
return support.sendError(res, 500, 'Unable to create post.', 'COMMUNITY_POST_CREATE_FAILED');
}
}

// ============================================================
// POST /api/community/posts/:postId/like (auth) -- toggle
// ============================================================
async function toggleLike(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

const user = _userFrom(req);
if (!user.userId) {
return support.sendError(res, 401, 'Authentication required.', 'AUTH_REQUIRED');
}

try {
const { liked, persistedTo } = await community.toggleLike({
postId: req.params.postId,
userId: user.userId,
});

// Return the post so the client can sync its optimistic state.
const { post } = await community.getPost(req.params.postId);
return support.sendSuccess(
res,
{ liked, post },
{ meta: { persistedTo } }
);
} catch (error) {
logger.error('communityController.toggleLike failed', { error: error.message });
return support.sendError(res, 500, 'Unable to update like.', 'COMMUNITY_LIKE_FAILED');
}
}

// ============================================================
// GET /api/community/posts/:postId/comments
// ============================================================
async function listComments(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

try {
const { items, source } = await community.listComments(req.params.postId);
return support.sendSuccess(
res,
{ items },
{ meta: { source, count: items.length } }
);
} catch (error) {
logger.error('communityController.listComments failed', { error: error.message });
return support.sendError(res, 500, 'Unable to load comments.', 'COMMUNITY_COMMENTS_FAILED');
}
}

// ============================================================
// POST /api/community/posts/:postId/comments (auth)
// ============================================================
async function createComment(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

const user = _userFrom(req);
if (!user.userId) {
return support.sendError(res, 401, 'Authentication required.', 'AUTH_REQUIRED');
}

try {
const { comment, persistedTo } = await community.createComment({
postId: req.params.postId,
userId: user.userId,
userName: user.name || user.email,
content: req.body.content,
});
return support.sendCreated(res, { comment }, { meta: { persistedTo } });
} catch (error) {
if (error.statusCode === 400) {
return support.sendError(res, 400, error.message, error.code || 'VALIDATION_ERROR');
}
logger.error('communityController.createComment failed', { error: error.message });
return support.sendError(res, 500, 'Unable to add comment.', 'COMMUNITY_COMMENT_FAILED');
}
}

// ============================================================
// GET /api/community/leaderboard
// ============================================================
async function leaderboard(req, res) {
const errors = validationResult(req);
if (!errors.isEmpty()) return support.sendValidationError(res, errors.array());

try {
// currentUserId from auth if present, else from query (optional).
const currentUserId = req.user?.userId ?? req.query.currentUserId;

const result = await community.getLeaderboard({
timeframe: req.query.timeframe,
currentUserId,
limit: req.query.limit,
});
return support.sendSuccess(
res,
{
timeframe: result.timeframe,
items: result.items,
currentUserRank: result.currentUserRank,
},
{ meta: { source: result.source } }
);
} catch (error) {
logger.error('communityController.leaderboard failed', { error: error.message });
return support.sendError(res, 500, 'Unable to load leaderboard.', 'COMMUNITY_LEADERBOARD_FAILED');
}
}

module.exports = {
listFeed,
getPost,
createPost,
toggleLike,
listComments,
createComment,
leaderboard,
};
Loading
Loading