From bc6f6615760418855bf8da8a0add496b979244a4 Mon Sep 17 00:00:00 2001 From: Dusk Date: Wed, 13 Oct 2021 17:32:56 -0400 Subject: [PATCH] tickets 3, 4, 5, and 6 --- src/models/Comment.ts | 112 +++++++-------- src/models/Post.ts | 230 ++++++++++++++++--------------- src/models/Reaction.ts | 118 ++++++++-------- src/services/TextService.test.ts | 109 +++++++-------- src/services/TextService.ts | 102 +++++++------- 5 files changed, 321 insertions(+), 350 deletions(-) diff --git a/src/models/Comment.ts b/src/models/Comment.ts index 1f0e82e..0bfdceb 100644 --- a/src/models/Comment.ts +++ b/src/models/Comment.ts @@ -1,60 +1,52 @@ -import mongoose, { Document, PopulatedDoc, Schema } from 'mongoose'; - -import TextService from '../services/TextService'; -import { Model } from '../utils/constants'; -import { BaseModel, ID } from '../utils/types'; -import Post, { PostDocument } from './Post'; -import User, { UserDocument } from './User'; - -/** - * TODO: (5.01) - * - Read this interface. - * - Delete this comment once you've done so. - */ -interface IComment extends BaseModel { - /** - * User that is associated with the creation of the comment. - */ - author: PopulatedDoc; - - /** - * Text content of the comment. - */ - content: string; - - /** - * Post that the comment was created on. - */ - post: PopulatedDoc; -} - -export type CommentDocument = Document<{}, {}, IComment> & IComment; - -const commentSchema: Schema = new Schema( - { - /** - * (5.02) TODO: - * - Create the schema for the Comments that we'll save in the database using - * the interface above as a reference. - * - Delete this comment and the example field. - * - Add comment(s) to explain your work. - */ - exampleField: { ref: Model.USER, required: false, type: ID, unique: false } - }, - { timestamps: true } -); - -commentSchema.pre('save', function () { - if (this.isNew) { - /** - * TODO: (6.05) - * - Send a text to the author of the post notifying them that a podmate - * commented under it! - */ - } -}); - -const Comment: mongoose.Model = - mongoose.model(Model.COMMENT, commentSchema); - -export default Comment; +import mongoose, { Document, PopulatedDoc, Schema } from 'mongoose'; + +import TextService from '../services/TextService'; +import { Model } from '../utils/constants'; +import { BaseModel, ID } from '../utils/types'; +import Post, { PostDocument } from './Post'; +import User, { UserDocument } from './User'; + +interface IComment extends BaseModel { + /** + * User that is associated with the creation of the comment. + */ + author: PopulatedDoc; + + /** + * Text content of the comment. + */ + content: string; + + /** + * Post that the comment was created on. + */ + post: PopulatedDoc; +} + +export type CommentDocument = Document<{}, {}, IComment> & IComment; + +const commentSchema: Schema = new Schema( + { + author: { ref: Model.USER, required: true, type: ID }, + content: { required: true, type: String }, + post: { ref: Model.POST, required: true, type: ID } + }, + { timestamps: true } +); + +commentSchema.pre('save', async function () { + if (this.isNew) { + const post: PostDocument = await Post.findById(this.post); + const postAuthor: UserDocument = await User.findById(post.author); + + TextService.sendText({ + message: 'One of your podmates commented on your post', + to: postAuthor.phoneNumber + }); + } +}); + +const Comment: mongoose.Model = + mongoose.model(Model.COMMENT, commentSchema); + +export default Comment; diff --git a/src/models/Post.ts b/src/models/Post.ts index a12f147..0af3040 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -1,114 +1,116 @@ -import mongoose, { Document, PopulatedDoc, Schema } from 'mongoose'; - -import TextService from '../services/TextService'; -import { Model } from '../utils/constants'; -import { BaseModel, ID } from '../utils/types'; -import { CommentDocument } from './Comment'; -import { ReactionDocument } from './Reaction'; -import User, { UserDocument } from './User'; - -/** - * TODO: (3.01) - * - Read this enum. - * - Delete this comment. - */ -export enum PostType { - HELP = 'HELP', // Asking for help... - TIL = 'TIL', // Today I learned... - WIN = 'WIN' // Sharing a win... -} - -/** - * TODO: (3.02) - * - Read this interface. - * - Delete this comment once you've done so. - */ -interface IPost extends BaseModel { - /** - * User that is associated with the creation of the post. - */ - author: PopulatedDoc; - - /** - * List of comments that were created on the post. - */ - comments: PopulatedDoc[]; - - /** - * Text content of the post. - */ - content: string; - - /** - * List of reactions that were created on the reaction. - */ - reactions: PopulatedDoc[]; - - /** - * Type of the post that was created. This can be null, if no PostType - * if specified. - */ - type?: PostType; -} - -export type PostDocument = Document<{}, {}, IPost> & IPost; - -const postSchema: Schema = new Schema( - { - /** - * TODO: (3.03) - * - Create the schema for the Posts that we'll save in the database using - * the interface above as a reference. - * - Delete this comment and the example field. - * - Add comment(s) to explain your work. - */ - exampleField: { required: true, type: String } - }, - { - timestamps: true, - toJSON: { virtuals: true }, - toObject: { virtuals: true } - } -); - -const sendNotification = async function ( - author: PopulatedDoc -) { - /** - * TODO: (6.04) - * - Send a text to all the users except for the author of this post letting - * them know that their podmate shared an update! - */ -}; - -postSchema.pre('save', function () { - if (this.isNew) { - sendNotification(this.author); - } -}); - -// Creates a "virtual" property on the Post model called 'comments'. By -// default, this sorts comments by the createdAt in ascending order (AKA we -// want to see newer comments last). -postSchema.virtual('comments', { - foreignField: 'post', - localField: '_id', - options: { sort: { createdAt: 1 } }, - ref: Model.COMMENT -}); - -// Similar to above, creates a "virtual" property called 'reactions' and we -// want to sort these in ascending order by their creation date/time. -postSchema.virtual('reactions', { - foreignField: 'post', - localField: '_id', - options: { sort: { createdAt: 1 } }, - ref: Model.REACTION -}); - -const Post: mongoose.Model = mongoose.model( - Model.POST, - postSchema -); - -export default Post; +import mongoose, { Document, PopulatedDoc, Schema } from 'mongoose'; +import { MemberContext } from 'twilio/lib/rest/api/v2010/account/queue/member'; + +import TextService from '../services/TextService'; +import { Model } from '../utils/constants'; +import { BaseModel, ID } from '../utils/types'; +import { CommentDocument } from './Comment'; +import { ReactionDocument } from './Reaction'; +import User, { UserDocument } from './User'; + +export enum PostType { + HELP = 'HELP', // Asking for help... + TIL = 'TIL', // Today I learned... + WIN = 'WIN' // Sharing a win... +} +interface IPost extends BaseModel { + /** + * User that is associated with the creation of the post. + */ + author: PopulatedDoc; + + /** + * List of comments that were created on the post. + */ + comments: PopulatedDoc[]; + + /** + * Text content of the post. + */ + content: string; + + /** + * List of reactions that were created on the reaction. + */ + reactions: PopulatedDoc[]; + + /** + * Type of the post that was created. This can be null, if no PostType + * if specified. + */ + type?: PostType; +} + +export type PostDocument = Document<{}, {}, IPost> & IPost; + +const postSchema: Schema = new Schema( + { + /** + * TODO: (3.03) + * - Create the schema for the Posts that we'll save in the database using + * the interface above as a reference. + * - Delete this comment and the example field. + * - Add comment(s) to explain your work. + */ + author: { ref: Model.USER, required: true, type: ID }, + content: { required: true, type: String }, + type: { required: false, type: string } + }, + { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } + } +); + +const sendNotification = async function ( + author: PopulatedDoc +) { + /** + * TODO: (6.04) + * - Send a text to all the users except for the author of this post letting + * them know that their podmate shared an update! + */ + const allUsers: UserDocument[] = await User.find(); + + allUsers.map((user) => { + if (user !== author) { + TextService.sendText({ + message: 'One of your podmates has shared a post', + to: user.phoneNumber + }); + } + }); +}; + +postSchema.pre('save', function () { + if (this.isNew) { + sendNotification(this.author); + } +}); + +// Creates a "virtual" property on the Post model called 'comments'. By +// default, this sorts comments by the createdAt in ascending order (AKA we +// want to see newer comments last). +postSchema.virtual('comments', { + foreignField: 'post', + localField: '_id', + options: { sort: { createdAt: 1 } }, + ref: Model.COMMENT +}); + +// Similar to above, creates a "virtual" property called 'reactions' and we +// want to sort these in ascending order by their creation date/time. +postSchema.virtual('reactions', { + foreignField: 'post', + localField: '_id', + options: { sort: { createdAt: 1 } }, + ref: Model.REACTION +}); + +const Post: mongoose.Model = mongoose.model( + Model.POST, + postSchema +); + +export default Post; diff --git a/src/models/Reaction.ts b/src/models/Reaction.ts index c32b1c0..a748438 100644 --- a/src/models/Reaction.ts +++ b/src/models/Reaction.ts @@ -1,64 +1,54 @@ -import mongoose, { Document, PopulatedDoc, Schema } from 'mongoose'; - -import { Model } from '../utils/constants'; -import { BaseModel, ID } from '../utils/types'; -import { PostDocument } from './Post'; -import { UserDocument } from './User'; - -/** - * TODO: (4.01) - * - Read this enum. - * - Delete this comment. - */ -export enum ReactionType { - FIRE = 'FIRE', // 🔥 - HEART = 'HEART', // 💖 - HUNDRED = 'HUNDRED', // 💯 - LAUGH = 'LAUGH', // 😂 - SAD = 'SAD' // 😢 -} - -/** - * TODO: (4.02) - * - Read this interface. - * - Delete this comment once you've done so. - */ -interface IReaction extends BaseModel { - /** - * Post that was "reacted" to. - */ - post: PopulatedDoc; - - /** - * Type of the reaction, which corresponds to an emoji, as seen above. - * - * @default ReactionType.HEART - */ - type: ReactionType; - - /** - * User that made the reaction. - */ - user: PopulatedDoc; -} - -export type ReactionDocument = Document<{}, {}, IReaction> & IReaction; - -const reactionSchema: Schema = new Schema( - { - /** - * TODO: (3.03) - * - Create the schema for the Reactions that we'll save in the database - * using the interface above as a reference. - * - Delete this comment and the example field. - * - Add comment(s) to explain your work. - */ - exampleField: { required: true, type: String } - }, - { timestamps: true } -); - -const Reaction: mongoose.Model = - mongoose.model(Model.REACTION, reactionSchema); - -export default Reaction; +import mongoose, { Document, PopulatedDoc, Schema } from 'mongoose'; + +import { Model } from '../utils/constants'; +import { BaseModel, ID } from '../utils/types'; +import { PostDocument } from './Post'; +import { UserDocument } from './User'; + +/** + * TODO: (4.01) + * - Read this enum. + * - Delete this comment. + */ +export enum ReactionType { + FIRE = 'FIRE', // 🔥 + HEART = 'HEART', // 💖 + HUNDRED = 'HUNDRED', // 💯 + LAUGH = 'LAUGH', // 😂 + SAD = 'SAD' // 😢 +} + +interface IReaction extends BaseModel { + /** + * Post that was "reacted" to. + */ + post: PopulatedDoc; + + /** + * Type of the reaction, which corresponds to an emoji, as seen above. + * + * @default ReactionType.HEART + */ + type: ReactionType; + + /** + * User that made the reaction. + */ + user: PopulatedDoc; +} + +export type ReactionDocument = Document<{}, {}, IReaction> & IReaction; + +const reactionSchema: Schema = new Schema( + { + post: { ref: Model.POST, required: true, type: ID }, + type: { default: ReactionType.HEART, required: true, type: String }, + user: { ref: Model.USER, required: true, type: ID } + }, + { timestamps: true } +); + +const Reaction: mongoose.Model = + mongoose.model(Model.REACTION, reactionSchema); + +export default Reaction; diff --git a/src/services/TextService.test.ts b/src/services/TextService.test.ts index 9dc4894..95eae0c 100644 --- a/src/services/TextService.test.ts +++ b/src/services/TextService.test.ts @@ -1,58 +1,51 @@ -import { APP } from '../utils/constants'; -import TextService, { client } from './TextService'; - -/** - * TODO: (6.05) - * - Remove the ".skip" from the following function. - * - Go to your terminal and run the following command: - * npm run test TextService - * - Delete this comment. - */ -describe.skip('TextService.sendText()', () => { - // Mock the twilio "sending" functionality. - client.messages.create = jest.fn(); - - afterEach(() => { - // As a fail safe, ensure that this is set back to false (for one of the) - // tests we need this to be true, but we don't want all tests like this! - APP.IS_PRODUCTION = false; - }); - - test('If the environment IS NOT production, should not call the Twilio API.', async () => { - const success: boolean = await TextService.sendText({ - message: '', - to: '' - }); - - expect(success).toBe(true); - expect(client.messages.create).not.toBeCalled(); - }); - - test('Should call the Twilio API if there is no error, should return true.', async () => { - // This has to be true in order for client.messages.create to be called... - APP.IS_PRODUCTION = true; - - const success: boolean = await TextService.sendText({ - message: '', - to: '' - }); - - expect(client.messages.create).toBeCalled(); - expect(success).toBe(true); - }); - - test('If the text failed to send, should return false.', async () => { - // This has to be true in order for client.messages.create to be called... - APP.IS_PRODUCTION = true; - - client.messages.create = jest.fn().mockRejectedValue(false); - - const success: boolean = await TextService.sendText({ - message: '', - to: '' - }); - - expect(client.messages.create).toBeCalled(); - expect(success).toBe(false); - }); -}); +import { APP } from '../utils/constants'; +import TextService, { client } from './TextService'; + +describe('TextService.sendText()', () => { + // Mock the twilio "sending" functionality. + client.messages.create = jest.fn(); + + afterEach(() => { + // As a fail safe, ensure that this is set back to false (for one of the) + // tests we need this to be true, but we don't want all tests like this! + APP.IS_PRODUCTION = false; + }); + + test('If the environment IS NOT production, should not call the Twilio API.', async () => { + const success: boolean = await TextService.sendText({ + message: '', + to: '' + }); + + expect(success).toBe(true); + expect(client.messages.create).not.toBeCalled(); + }); + + test('Should call the Twilio API if there is no error, should return true.', async () => { + // This has to be true in order for client.messages.create to be called... + APP.IS_PRODUCTION = true; + + const success: boolean = await TextService.sendText({ + message: '', + to: '' + }); + + expect(client.messages.create).toBeCalled(); + expect(success).toBe(true); + }); + + test('If the text failed to send, should return false.', async () => { + // This has to be true in order for client.messages.create to be called... + APP.IS_PRODUCTION = true; + + client.messages.create = jest.fn().mockRejectedValue(false); + + const success: boolean = await TextService.sendText({ + message: '', + to: '' + }); + + expect(client.messages.create).toBeCalled(); + expect(success).toBe(false); + }); +}); diff --git a/src/services/TextService.ts b/src/services/TextService.ts index f5565d9..99ec206 100644 --- a/src/services/TextService.ts +++ b/src/services/TextService.ts @@ -1,54 +1,48 @@ -import twilio, { Twilio } from 'twilio'; - -import { APP } from '../utils/constants'; - -/** - * TODO: (6.01): - * - Add your twilio account id, auth token, and phone number to your - * .env.development file. - */ -export const client: Twilio = twilio( - APP.TWILIO_ACCOUNT_SID, - APP.TWILIO_AUTH_TOKEN -); - -type SendTextArgs = { - message: string; - to: string; -}; - -/** - * Sends a text message to the given recipient with the given message. Returns - * true if the message was sent successfully, and false if there was an - * error. - * - * If the environment is not production, this function does nothing and returns - * true. - * - * @param args.message - Text content to send in message. - * @param args.to - Phone number to send the message to. - */ -const sendText = async ({ message, to }: SendTextArgs): Promise => { - // Don't send texts unless it is production environment. If you want texts - // to send while in development, simply comment out this line. - // if (!APP.IS_PRODUCTION) return true; - - /** - * TODO: (6.03) - * - Send the {message} to {to} and return true if we successfully do so. - */ - - try { - // Send the text - } catch (e) { - // What should be return if sending the text was unsuccessful? - } - - return true; -}; - -const TextService = { - sendText -}; - -export default TextService; +import twilio, { Twilio } from 'twilio'; + +import { APP } from '../utils/constants'; + +export const client: Twilio = twilio( + APP.TWILIO_ACCOUNT_SID, + APP.TWILIO_AUTH_TOKEN +); + +type SendTextArgs = { + message: string; + to: string; +}; + +/** + * Sends a text message to the given recipient with the given message. Returns + * true if the message was sent successfully, and false if there was an + * error. + * + * If the environment is not production, this function does nothing and returns + * true. + * + * @param args.message - Text content to send in message. + * @param args.to - Phone number to send the message to. + */ +const sendText = async ({ message, to }: SendTextArgs): Promise => { + // Don't send texts unless it is production environment. If you want texts + // to send while in development, simply comment out this line. + if (!APP.IS_PRODUCTION) return true; + + try { + await client.messages.create({ + body: message, + from: APP.TWILIO_PHONE_NUMBER, + to + }); + } catch (e) { + return false; + } + + return true; +}; + +const TextService = { + sendText +}; + +export default TextService;