Skip to content

Commit 6a67258

Browse files
committed
Fix env usage and mongoose model type issues
1 parent e6040fd commit 6a67258

File tree

10 files changed

+332
-69
lines changed

10 files changed

+332
-69
lines changed

functions/src/api/notify-email.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// functions/src/api/notify-email.ts
2+
import type { VercelRequest, VercelResponse } from '@vercel/node';
3+
import { notifyEmail, TweetData } from '../notify-email';
4+
5+
/**
6+
* Vercel serverless function to send email notifications
7+
* Expects POST request with JSON body: { id, text, createdBy, username, images?, parent? }
8+
*/
9+
export default async function handler(req: VercelRequest, res: VercelResponse) {
10+
try {
11+
if (req.method !== 'POST') {
12+
return res.status(405).json({ error: 'Method not allowed' });
13+
}
14+
15+
const tweetData: TweetData = req.body;
16+
17+
if (!tweetData || !tweetData.id || !tweetData.text || !tweetData.createdBy || !tweetData.username) {
18+
return res.status(400).json({ error: 'Invalid tweet data' });
19+
}
20+
21+
await notifyEmail(tweetData);
22+
23+
res.status(200).json({ message: 'Notification email sent' });
24+
} catch (error) {
25+
console.error('Error sending notification email:', error);
26+
res.status(500).json({ error: 'Internal Server Error' });
27+
}
28+
}

functions/src/lib/utils.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1-
import * as functions from 'firebase-functions';
1+
// functions/src/lib/utils.ts
2+
import mongoose from 'mongoose';
3+
import { MONGO_URI } from './env';
24

3-
const regionalFunctions = functions.region('asia-southeast2');
5+
/**
6+
* Connect to MongoDB
7+
*/
8+
export async function connectMongo() {
9+
if (!mongoose.connection.readyState) {
10+
await mongoose.connect(MONGO_URI, {
11+
autoIndex: true,
12+
dbName: 'twitter_clone',
13+
});
14+
console.log('✅ Connected to MongoDB');
15+
}
16+
}
417

5-
export { firestore } from 'firebase-admin';
6-
export { functions, regionalFunctions };
18+
/**
19+
* Optional helper: delay (can replace Firebase async helpers)
20+
*/
21+
export function delay(ms: number) {
22+
return new Promise(resolve => setTimeout(resolve, ms));
23+
}
Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
// functions/src/mongo/models/Hashtag.ts
2-
import { Schema, model, models } from 'mongoose';
2+
import { Schema, model, models, Types, Model, Document } from 'mongoose';
33

4-
const HashtagSchema = new Schema({
5-
tag: { type: String, required: true, unique: true },
6-
tweetIds: { type: [String], default: [] },
7-
count: { type: Number, default: 1 },
4+
// Define interface for a Hashtag document
5+
interface HashtagDocument extends Document {
6+
tag: string;
7+
count: number;
8+
tweets: Types.ObjectId[];
9+
createdAt: Date;
10+
}
11+
12+
const HashtagSchema = new Schema<HashtagDocument>({
13+
tag: { type: String, required: true, unique: true }, // the hashtag text
14+
count: { type: Number, default: 1 }, // number of times used
15+
tweets: [{ type: Types.ObjectId, ref: 'Tweet' }], // references to Tweet documents
16+
createdAt: { type: Date, default: Date.now },
817
});
918

10-
export const Hashtag = models.Hashtag || model('Hashtag', HashtagSchema);
19+
// Fix TS union type issue while keeping logic original
20+
export const Hashtag: Model<HashtagDocument> = models.Hashtag || model<HashtagDocument>('Hashtag', HashtagSchema);

functions/src/mongo/models/Tweet.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { Schema, model, models } from 'mongoose';
1+
import { Schema, model, models, Types } from 'mongoose';
22

33
const TweetSchema = new Schema({
4+
firebaseId: { type: String, required: true, unique: true }, // link to Firestore
45
text: { type: String, required: true },
56
createdBy: { type: String, required: true },
67
username: { type: String, required: true },
78
userAvatar: { type: String, default: "" },
89
images: { type: [String], default: [] },
910
videos: { type: [String], default: [] },
10-
tags: { type: [String], default: [] },
11+
hashtags: { type: [String], default: [] }, // renamed from tags
1112
likes: { type: Number, default: 0 },
1213
comments: { type: Number, default: 0 },
1314
retweets: { type: Number, default: 0 },

functions/src/mongo/updateTags.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,76 @@
22
import { Hashtag } from './models/Hashtag';
33
import { Mention } from './models/Mention';
44
import { Trending } from './models/Trending';
5+
import { Tweet } from './models/Tweet';
6+
import { Types } from 'mongoose';
57

6-
export async function processTags(tweetId: string, text: string) {
8+
interface TweetData {
9+
firebaseId: string;
10+
text: string;
11+
createdBy: string;
12+
username: string;
13+
userAvatar?: string;
14+
images?: string[];
15+
videos?: string[];
16+
}
17+
18+
export async function processTags(tweetData: TweetData) {
19+
const { firebaseId, text, createdBy, username, userAvatar, images = [], videos = [] } = tweetData;
20+
21+
// 1️⃣ Insert tweet into Mongo (or skip if already exists)
22+
let mongoTweet = await Tweet.findOne({ firebaseId });
23+
if (!mongoTweet) {
24+
mongoTweet = await Tweet.create({
25+
firebaseId,
26+
text,
27+
createdBy,
28+
username,
29+
userAvatar: userAvatar || "",
30+
images,
31+
videos,
32+
hashtags: [],
33+
});
34+
}
35+
36+
// 2️⃣ Extract hashtags and mentions
737
const hashtags = Array.from(text.matchAll(/#(\w+)/g)).map(m => m[1].toLowerCase());
838
const mentions = Array.from(text.matchAll(/@(\w+)/g)).map(m => m[1].toLowerCase());
939

10-
// Hashtags
40+
// 3️⃣ Update hashtags in Mongo
1141
for (const tag of hashtags) {
12-
await Hashtag.findOneAndUpdate(
42+
const hashtag = await Hashtag.findOneAndUpdate(
1343
{ tag },
14-
{ $addToSet: { tweetIds: tweetId }, $inc: { count: 1 } },
15-
{ upsert: true }
44+
{ $addToSet: { tweets: mongoTweet._id }, $inc: { count: 1 } },
45+
{ upsert: true, new: true }
1646
);
47+
mongoTweet.hashtags.push(tag);
48+
49+
// Update Trending collection
1750
await Trending.findOneAndUpdate(
1851
{ type: 'hashtag', value: tag },
1952
{ $inc: { score: 1 } },
2053
{ upsert: true }
2154
);
2255
}
2356

24-
// Mentions
57+
// 4️⃣ Update mentions in Mongo
2558
for (const username of mentions) {
2659
await Mention.findOneAndUpdate(
2760
{ username },
28-
{ $addToSet: { tweetIds: tweetId }, $inc: { count: 1 } },
61+
{ $addToSet: { tweetIds: mongoTweet._id }, $inc: { count: 1 } },
2962
{ upsert: true }
3063
);
64+
65+
// Update Trending collection
3166
await Trending.findOneAndUpdate(
3267
{ type: 'mention', value: username },
3368
{ $inc: { score: 1 } },
3469
{ upsert: true }
3570
);
3671
}
72+
73+
// 5️⃣ Save updated tweet hashtags array
74+
await mongoTweet.save();
75+
76+
return mongoTweet;
3777
}

functions/src/notify-email.ts

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,53 @@
1+
// functions/src/notify-email.ts
12
import { createTransport } from 'nodemailer';
2-
import { firestore, functions, regionalFunctions } from './lib/utils';
3+
import { connectMongo } from './lib/utils';
34
import { EMAIL_API, EMAIL_API_PASSWORD, TARGET_EMAIL } from './lib/env';
4-
import type { Tweet, User } from './types';
5-
6-
export const notifyEmail = regionalFunctions.firestore
7-
.document('tweets/{tweetId}')
8-
.onCreate(async (snapshot): Promise<void> => {
9-
functions.logger.info('Sending notification email.');
10-
11-
const { text, createdBy, images, parent } = snapshot.data() as Tweet;
12-
13-
const imagesLength = images?.length ?? 0;
14-
15-
const { name, username } = (
16-
await firestore().doc(`users/${createdBy}`).get()
17-
).data() as User;
18-
19-
const client = createTransport({
20-
service: 'Gmail',
21-
auth: {
22-
user: EMAIL_API.value(),
23-
pass: EMAIL_API_PASSWORD.value()
24-
}
25-
});
26-
27-
const tweetLink = `https://twitter-clone-ccrsxx.vercel.app/tweet/${snapshot.id}`;
28-
29-
const emailHeader = `New Tweet${
30-
parent ? ' reply' : ''
31-
} from ${name} (@${username})`;
5+
import { Tweet } from './mongo/models/Tweet';
6+
7+
export interface TweetData {
8+
id: string;
9+
text: string;
10+
createdBy: string;
11+
username: string;
12+
images?: string[];
13+
parent?: string;
14+
}
15+
16+
/**
17+
* Send notification email when a new tweet is created
18+
*/
19+
export async function notifyEmail(tweetData: TweetData) {
20+
const { id, text, createdBy, username, images = [], parent } = tweetData;
21+
22+
// Connect to MongoDB
23+
await connectMongo();
24+
25+
// Fetch user from MongoDB (fallback to payload if not found)
26+
const user = await Tweet.findOne({ _id: createdBy });
27+
const name = user?.username || username;
28+
29+
const client = createTransport({
30+
service: 'Gmail',
31+
auth: {
32+
user: EMAIL_API.value(),
33+
pass: EMAIL_API_PASSWORD.value(),
34+
},
35+
});
3236

33-
const emailText = `${text ?? 'No text provided'}${
34-
images ? ` (${imagesLength} image${imagesLength > 1 ? 's' : ''})` : ''
35-
}\n\nLink to Tweet: ${tweetLink}\n\n- Firebase Function.`;
37+
const imagesLength = images.length;
38+
const tweetLink = `https://twitter-clone-ccrsxx.vercel.app/tweet/${id}`;
3639

37-
await client.sendMail({
38-
from: EMAIL_API.value(),
39-
to: TARGET_EMAIL.value(),
40-
subject: emailHeader,
41-
text: emailText
42-
});
40+
const emailHeader = `New Tweet${parent ? ' reply' : ''} from ${name} (@${username})`;
41+
const emailText = `${text ?? 'No text provided'}${
42+
imagesLength ? ` (${imagesLength} image${imagesLength > 1 ? 's' : ''})` : ''
43+
}\n\nLink to Tweet: ${tweetLink}\n\n- Vercel Serverless Function`;
4344

44-
functions.logger.info('Notification email sent.');
45+
await client.sendMail({
46+
from: EMAIL_API.value(),
47+
to: TARGET_EMAIL.value(),
48+
subject: emailHeader,
49+
text: emailText,
4550
});
51+
52+
console.log(`📧 Notification email sent for tweet ${id}`);
53+
}

src/lib/api/tweets.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,52 @@
11
// src/lib/api/tweets.ts
2-
import { getFunctions, httpsCallable } from "firebase/functions";
32

4-
// Example createTweet function placeholder — replace with your actual implementation
5-
async function createTweet({ text, file, userId }: any) {
6-
// Save tweet in Firestore or wherever you store it
7-
return { id: "some-tweet-id", text };
3+
export interface CreateTweetParams {
4+
text: string;
5+
file?: string[]; // Array of uploaded image URLs
6+
userId: string; // ID of the user creating the tweet
7+
username: string; // Username of the creator
8+
parent?: string; // Optional parent tweet ID (if this is a reply)
89
}
910

10-
export async function createTweetWithTags({ text, file, userId }: any) {
11+
export async function createTweetWithTags({
12+
text,
13+
file,
14+
userId,
15+
username,
16+
parent
17+
}: CreateTweetParams) {
1118
// 1️⃣ Create the tweet locally
12-
const tweet = await createTweet({ text, file, userId });
19+
const tweet = {
20+
id: crypto.randomUUID(), // Unique local ID
21+
text,
22+
createdBy: userId,
23+
username,
24+
images: file ?? [],
25+
parent
26+
};
1327

14-
// 2️⃣ Call Firebase Function to handle hashtags & mentions in Mongo
15-
const functions = getFunctions();
16-
const createTweetMongo = httpsCallable(functions, "createTweetMongo");
17-
await createTweetMongo({ tweetId: tweet.id, text: tweet.text });
28+
// 2️⃣ Call both Vercel APIs in parallel
29+
try {
30+
await Promise.all([
31+
// Mirror tweet in Mongo & process hashtags/mentions
32+
fetch('/api/createTweetMongo', {
33+
method: 'POST',
34+
headers: { 'Content-Type': 'application/json' },
35+
body: JSON.stringify(tweet),
36+
}),
37+
38+
// Send notification email
39+
fetch('/api/notify-email', {
40+
method: 'POST',
41+
headers: { 'Content-Type': 'application/json' },
42+
body: JSON.stringify(tweet),
43+
})
44+
]);
45+
} catch (error) {
46+
console.error('Error processing tweet on server:', error);
47+
// Optionally handle failure: retry, queue, or notify admin
48+
}
1849

50+
// 3️⃣ Return local tweet immediately
1951
return tweet;
2052
}

src/pages/api/createTweetMongo.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// src/pages/api/createTweetMongo.ts
2+
import type { NextApiRequest, NextApiResponse } from 'next';
3+
import mongoose from 'mongoose';
4+
import { processTags } from '../../../functions/src/mongo/updateTags';
5+
6+
// Use process.env for server-side env variables
7+
const MONGO_URI = process.env.MONGO_URI!;
8+
9+
// Ensure Mongo is connected
10+
async function connectMongo() {
11+
if (mongoose.connection.readyState === 0) {
12+
await mongoose.connect(MONGO_URI);
13+
}
14+
}
15+
16+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
17+
if (req.method !== 'POST') {
18+
return res.status(405).json({ error: 'Method not allowed' });
19+
}
20+
21+
try {
22+
const { tweetId, text, createdBy, username, userAvatar, images, videos } = req.body;
23+
24+
if (!tweetId || !text || !createdBy || !username) {
25+
return res.status(400).json({ error: 'Missing required fields' });
26+
}
27+
28+
await connectMongo();
29+
30+
const mongoTweet = await processTags({
31+
firebaseId: tweetId,
32+
text,
33+
createdBy,
34+
username,
35+
userAvatar,
36+
images,
37+
videos
38+
});
39+
40+
return res.status(200).json({ success: true, mongoTweet });
41+
} catch (err) {
42+
console.error('Error in createTweetMongo:', err);
43+
return res.status(500).json({ error: 'Internal server error' });
44+
}
45+
}

0 commit comments

Comments
 (0)