diff --git a/organizations.json b/organizations.json index ca32b50..4e63ec4 100644 --- a/organizations.json +++ b/organizations.json @@ -1,20 +1,452 @@ { - "organizations": [{ + "organizations": [ + { "bio": "", - "name": "Women in Stem at Cornell", - "slug": "wicc", - "websiteURL": "https://wicc.cornell.edu/#/" - }, { + "name": "African Dance Repertoire", + "slug": "adr", + "websiteURL": "https://www.instagram.com/adrcornell/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "After Eight", + "slug": "after8", + "websiteURL": "http://cuchorus.com/after-eight", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Alt Protein", + "slug": "protein", + "websiteURL": "https://www.instagram.com/altprocornell/?hl=en", + "categorySlug": "foodDrinks" + }, + { "bio": "", "name": "AppDev", "slug": "appdev", - "websiteURL": "https://www.cornellappdev.com/" + "websiteURL": "https://www.cornellappdev.com/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Asian American Intervarsity", + "slug": "aaiv", + "websiteURL": "https://www.instagram.com/cornellaaiv/?hl=en", + "categorySlug": "spiritual" + }, + { + "bio": "", + "name": "Asian Pacific Student Union", + "slug": "capsu", + "websiteURL": "https://www.capsucornell.org/", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Assorted Aces", + "slug": "assortedaces", + "websiteURL": "https://linktr.ee/assortedaces", + "categorySlug": "dance" }, { "bio": "", "name": "Badminton Club", "slug": "badminton", - "websiteURL": "https://cornellbadminton.com/" + "websiteURL": "https://cornellbadminton.com/", + "categorySlug": "sports" + }, + { + "bio": "", + "name": "Bangladeshi Student Association", + "slug": "bsa", + "websiteURL": "https://www.instagram.com/cornellbsa/?hl=en", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Big Red Raas", + "slug": "bigredraas", + "websiteURL": "https://cornellbigredraas.github.io/", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Break Free", + "slug": "breakfree", + "websiteURL": "https://www.breakfreecornell.com/", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Campus Installation Artists", + "slug": "cia", + "websiteURL": null, + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Chinese Student Assocation", + "slug": "csa", + "websiteURL": "https://www.instagram.com/cornell_csa/?hl=en", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Cornell Bhangra", + "slug": "bhangra", + "websiteURL": "https://www.instagram.com/cornellbhangra/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Cornell Callbaxx", + "slug": "callbaxx", + "websiteURL": "https://www.instagram.com/thecallbaxx/?hl=en", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Cornell Chimes", + "slug": "cornellchimes", + "websiteURL": "https://chimes.cornell.edu/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Cornell Chinese Drama Society", + "slug": "ccds", + "websiteURL": "https://www.instagram.com/cornelljingyuan/", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Cornell Filipino Association", + "slug": "cfa", + "websiteURL": "https://www.instagram.com/cornellfilipino/?hl=en", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Cornell Fintech", + "slug": "fintech", + "websiteURL": "https://www.cornellfintechclub.com/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Cornell Hydroponics Club", + "slug": "hydroponics", + "websiteURL": "https://www.instagram.com/cornellhydroponicsclub/?hl=en", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Cornell Lion Dance", + "slug": "liondance", + "websiteURL": "https://www.instagram.com/cornell_liondance/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Cornell Nazaqat", + "slug": "nazaqat", + "websiteURL": "https://www.instagram.com/cornellnazaqat/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Cornell Sitara", + "slug": "sitara", + "websiteURL": "https://www.instagram.com/cornellsitara/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Cornell Taiwanese American Society", + "slug": "ctas", + "websiteURL": "https://cornelltas.com/", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Cornell Thrift", + "slug": "thrift", + "websiteURL": "https://www.instagram.com/cornellthrift/?hl=en", + "categorySlug": "awareness" + }, + { + "bio": "", + "name": "Cornell Touchtones", + "slug": "touchtones", + "websiteURL": "https://www.thetouchtones.com/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Cornell Vietnamese Association", + "slug": "cva", + "websiteURL": "https://www.instagram.com/cornellviet/?hl=en", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Cornell Wardrobe", + "slug": "wardrobe", + "websiteURL": "https://www.cornellwardrobe.org/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Cornell Wushu", + "slug": "wushu", + "websiteURL": "https://cornellwushu.github.io/", + "categorySlug": "sports" + }, + { + "bio": "", + "name": "CU Class councils", + "slug": "cucouncils", + "websiteURL": "https://www.instagram.com/cuclasscouncils/?hl=en", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "DTI", + "slug": "dti", + "websiteURL": "https://www.cornelldti.org/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "ESports at Cornell", + "slug": "esports", + "websiteURL": "https://esportsatcornell.com/index.php", + "categorySlug": "sports" + }, + { + "bio": "", + "name": "E-Motion", + "slug": "emotion", + "websiteURL": "https://www.instagram.com/cornellemotion/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Food Science Club", + "slug": "foodsci", + "websiteURL": "https://www.instagram.com/cornellfoodscience/?hl=en", + "categorySlug": "foodDrinks" + }, + { + "bio": "", + "name": "Food & Beverage Society", + "slug": "fbs", + "websiteURL": "https://www.instagram.com/cornell_fbs/?hl=en", + "categorySlug": "foodDrinks" + }, + { + "bio": "", + "name": "Free Space", + "slug": "freespace", + "websiteURL": null, + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Guild of Visual Arts", + "slug": "gva", + "websiteURL": "https://cornell.campusgroups.com/gva/home/", + "categorySlug": "art" + }, + { + "bio": "", + "name": "Hearsay A Cappella", + "slug": "hearsay", + "websiteURL": "https://www.hearsayacappella.com/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Hong Kong Student Assocation", + "slug": "hksa", + "websiteURL": "https://www.instagram.com/cornellhksa/?hl=en", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "International Student Union", + "slug": "isu", + "websiteURL": "https://isucornell.com/", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Japan US Association", + "slug": "jusa", + "websiteURL": "https://cornelljusa.wordpress.com/", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Korean American Student Association", + "slug": "kasa", + "websiteURL": "https://www.instagram.com/cornellkasa/?hl=en", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Last Call", + "slug": "lastcall", + "websiteURL": "https://menoflastcall.com/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Loko", + "slug": "loko", + "websiteURL": "https://www.instagram.com/loko_cornell/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Mediocre Melodies", + "slug": "mediocre", + "websiteURL": "https://mediocremelodies.com/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Medium Design Collective", + "slug": "mediumdesigncollective", + "websiteURL": "https://cornellmedium.design/", + "categorySlug": "art" + }, + { + "bio": "", + "name": "Mexican Student Association", + "slug": "mex", + "websiteURL": "https://cornell.campusgroups.com/mexsa/home/", + "categorySlug": "cultural" + }, + { + "bio": "", + "name": "Midnight Comedy", + "slug": "midnightcomedy", + "websiteURL": "https://www.midnightcomedytroupe.com/", + "categorySlug": "comedy" + }, + { + "bio": "", + "name": "Nothing But Treble", + "slug": "nbt", + "websiteURL": "https://www.nothingbuttreblecornell.com/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Pants Improv Comedy", + "slug": "pantsimprov", + "websiteURL": "https://pantsimprov.wixsite.com/comedy", + "categorySlug": "comedy" + }, + { + "bio": "", + "name": "Project Hope", + "slug": "projecthope", + "websiteURL": "http://projecthopecornell.weebly.com/", + "categorySlug": "awareness" + }, + { + "bio": "", + "name": "Residential Sustainability Leaders", + "slug": "rsl", + "websiteURL": "https://experience.cornell.edu/opportunities/residential-sustainability-leaders-rsls", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Rise Dance Group", + "slug": "rise", + "websiteURL": "https://www.risedancegroupcornell.com/", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Sabor Latino", + "slug": "sabor", + "websiteURL": "https://www.instagram.com/cornellsaborlatino/?hl=en", + "categorySlug": "dance" + }, + { + "bio": "", + "name": "Shimtah", + "slug": "shimtah", + "websiteURL": "https://sites.google.com/cornell.edu/shimtah/home?pli=1", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Tarana A Cappella", + "slug": "tarana", + "websiteURL": "https://www.cornelltarana.net/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "The Hangovers", + "slug": "hangovers", + "websiteURL": "http://www.hangovers.com/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "The Key Elements", + "slug": "keyelements", + "websiteURL": "https://ke-website.webflow.io/", + "categorySlug": "music" + }, + { + "bio": "", + "name": "Translator Interpreter Program", + "slug": "tip", + "websiteURL": "https://cornell.campusgroups.com/tip/home/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Underrepresented Minorities in Computing", + "slug": "urmc", + "websiteURL": "https://eship.cornell.edu/item/urmc/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Whistling Shrimp", + "slug": "whistlingshrimp", + "websiteURL": "https://thewhistlingshrimp.weebly.com/", + "categorySlug": "comedy" + }, + { + "bio": "", + "name": "Women in Stem at Cornell", + "slug": "wicc", + "websiteURL": "https://wicc.cornell.edu/#/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Women Leaders of Color", + "slug": "wlc", + "websiteURL": "https://cornell.campusgroups.com/wlocc/home/", + "categorySlug": "academic" + }, + { + "bio": "", + "name": "Yamatai", + "slug": "yamatai", + "websiteURL": "https://www.yamatai-taiko.com/", + "categorySlug": "music" } ] } \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 0c26c2e..9d1c813 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,18 +4,20 @@ import admin from 'firebase-admin'; import Express from 'express'; import { buildSchema } from 'type-graphql'; + import { ApolloServer } from 'apollo-server-express'; import ArticleResolver from './resolvers/ArticleResolver'; import ArticleRepo from './repos/ArticleRepo'; import { dbConnection } from './db/DBConnection'; +import FlyerRepo from './repos/FlyerRepo'; import FlyerResolver from './resolvers/FlyerResolver'; import MagazineRepo from './repos/MagazineRepo'; import MagazineResolver from './resolvers/MagazineResolver'; import NotificationRepo from './repos/NotificationRepo'; import OrganizationResolver from './resolvers/OrganizationResolver'; import PublicationResolver from './resolvers/PublicationResolver'; -import WeeklyDebriefRepo from './repos/WeeklyDebriefRepo'; import UserResolver from './resolvers/UserResolver'; +import WeeklyDebriefRepo from './repos/WeeklyDebriefRepo'; const main = async () => { const schema = await buildSchema({ @@ -76,13 +78,15 @@ const main = async () => { } async function setupTrendingArticleRefreshCron() { - // Refresh trending articles once + // Refresh trending articles, magazines, and flyers once ArticleRepo.refreshTrendingArticles(); MagazineRepo.refreshFeaturedMagazines(); - // Refresh trending articles 12 hours + FlyerRepo.refreshTrendingFlyers(); + // Refresh trending articles, magazines, and flyers every 12 hours cron.schedule('0 */12 * * *', async () => { ArticleRepo.refreshTrendingArticles(); MagazineRepo.refreshFeaturedMagazines(); + FlyerRepo.refreshTrendingFlyers(); }); } diff --git a/src/common/types.ts b/src/common/types.ts index 23f0a4c..d8b2d8f 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -30,3 +30,21 @@ export class PublicationSlug { this.slug = slug; } } + +@ObjectType({ description: 'ID of an Organization' }) +export class OrganizationID { + @Field() + @Property() + id: string; +} + +@ObjectType({ description: 'Slug of an Organization' }) +export class OrganizationSlug { + @Field() + @Property() + slug: string; + + constructor(slug: string) { + this.slug = slug; + } +} \ No newline at end of file diff --git a/src/entities/Flyer.ts b/src/entities/Flyer.ts index bb910ff..7d6801d 100644 --- a/src/entities/Flyer.ts +++ b/src/entities/Flyer.ts @@ -31,13 +31,13 @@ export class Flyer { @Property({ default: false }) nsfw: boolean; - @Field((type) => Organization) - @Property({ type: () => Organization }) - organization: Organization; + @Field((type) => [Organization]) + @Property({ type: () => [Organization] }) + organizations: [Organization]; - @Field() - @Property() - organizationSlug: string; + @Field((type) => [String]) + @Property({ type: () => [String] }) + organizationSlugs: [string]; @Field() @Property({ default: 0 }) diff --git a/src/entities/Organization.ts b/src/entities/Organization.ts index bc9c9ec..645594b 100644 --- a/src/entities/Organization.ts +++ b/src/entities/Organization.ts @@ -14,6 +14,10 @@ export class Organization { @Property() bio: string; + @Field() + @Property() + categorySlug: string; + @Field() @Property() name: string; diff --git a/src/repos/FlyerRepo.ts b/src/repos/FlyerRepo.ts index 43a427c..7555c34 100644 --- a/src/repos/FlyerRepo.ts +++ b/src/repos/FlyerRepo.ts @@ -53,7 +53,7 @@ const getFlyersByOrganizationSlug = async ( limit: number = DEFAULT_LIMIT, offset: number = DEFAULT_OFFSET, ): Promise<Flyer[]> => { - return FlyerModel.find({ 'organization.slug': slug }) + return FlyerModel.find({ organizationSlugs: slug }) .sort({ date: 'desc' }) .skip(offset) .limit(limit) @@ -68,7 +68,7 @@ const getFlyersByOrganizationSlugs = async ( offset: number = DEFAULT_OFFSET, ): Promise<Flyer[]> => { const uniqueSlugs = [...new Set(slugs)]; - return FlyerModel.find({ 'organization.slug': { $in: uniqueSlugs } }) + return FlyerModel.find({ organizationSlugs: { $in: uniqueSlugs } }) .sort({ date: 'desc' }) .skip(offset) .limit(limit) @@ -83,7 +83,7 @@ const getFlyersByOrganizationID = async ( offset: number = DEFAULT_OFFSET, ): Promise<Flyer[]> => { const organization = await (await OrganizationModel.findById(organizationID)).execPopulate(); - return FlyerModel.find({ 'organization.slug': organization.slug }) + return FlyerModel.find({ organizationSlugs: organization.slug }) .sort({ date: 'desc' }) .skip(offset) .limit(limit) diff --git a/src/repos/OrganizationRepo.ts b/src/repos/OrganizationRepo.ts index aa82695..6c3ce01 100644 --- a/src/repos/OrganizationRepo.ts +++ b/src/repos/OrganizationRepo.ts @@ -12,12 +12,13 @@ const addOrganizationsToDB = async (): Promise<void> => { const orgDocUpdates = []; for (const organization of organizationsJSON.organizations) { - const { bio, name, slug, websiteURL } = organization; + const { bio, name, slug, websiteURL, categorySlug } = organization; const orgDoc = Object.assign(new Organization(), { bio, name, slug, websiteURL, + categorySlug, }); // Add or update the organization created from the JSON @@ -29,6 +30,10 @@ const addOrganizationsToDB = async (): Promise<void> => { await Promise.all(orgDocUpdates); }; +const getOrganizationsByCategory = async (categorySlug: string): Promise<Organization[]> => { + return OrganizationModel.find({ categorySlug }); +}; + const getOrganizationByID = async (id: string): Promise<Organization> => { return OrganizationModel.findById(new ObjectId(id)); }; @@ -61,7 +66,7 @@ const getMostRecentFlyer = async (organization: Organization): Promise<Flyer> => // Due to the way Mongo interprets 'Organization' object, // Organization['_doc'] must be used to access fields of a Organization object return FlyerModel.findOne({ - organizationSlug: organization['_doc'].slug, // eslint-disable-line + organizationSlugs: organization['_doc'].slug, // eslint-disable-line }).sort({ date: 'desc' }); }; @@ -75,7 +80,7 @@ const getShoutouts = async (organization: Organization): Promise<number> => { // Due to the way Mongo interprets 'Organization' object, // Organization['_doc'] must be used to access fields of a Organization object const orgFlyers = await FlyerModel.find({ - organizationSlug: organization['_doc'].slug, // eslint-disable-line + organizationSlugs: organization['_doc'].slug, // eslint-disable-line }); return orgFlyers.reduce((acc, flyer) => { @@ -93,7 +98,7 @@ const getNumFlyers = async (organization: Organization): Promise<number> => { // Due to the way Mongo interprets 'Organization' object, // Organization['_doc'] must be used to access fields of a Organization object const orgFlyers = await FlyerModel.find({ - organizationSlug: organization['_doc'].slug, // eslint-disable-line + organizationSlugs: organization['_doc'].slug, // eslint-disable-line }); return orgFlyers.length; @@ -104,8 +109,9 @@ export default { getAllOrganizations, getMostRecentFlyer, getNumFlyers, + getOrganizationsByCategory, getOrganizationByID, - getOrganizationBySlug, getOrganizationsByIDs, + getOrganizationBySlug, getShoutouts, }; diff --git a/src/repos/UserRepo.ts b/src/repos/UserRepo.ts index 1997965..467602e 100644 --- a/src/repos/UserRepo.ts +++ b/src/repos/UserRepo.ts @@ -1,11 +1,12 @@ import { v4 as uuidv4 } from 'uuid'; import { Article } from '../entities/Article'; import ArticleRepo from './ArticleRepo'; -import { PublicationSlug } from '../common/types'; +import { OrganizationSlug, PublicationSlug } from '../common/types'; import { User, UserModel } from '../entities/User'; import { Magazine } from '../entities/Magazine'; import MagazineRepo from './MagazineRepo'; - +import { Flyer } from '../entities/Flyer'; +import FlyerRepo from './FlyerRepo'; /** * Create new user associated with deviceToken and followedPublicationsSlugs of deviceType. */ @@ -65,6 +66,37 @@ const unfollowPublication = async (uuid: string, pubSlug: string): Promise<User> return user; }; +/** + * Adds organization slug to user's followedOrganizations. + * Requires: the user is not already following the organization. + */ +const followOrganization = async (uuid: string, slug: string): Promise<User> => { + const user = await UserModel.findOne({ uuid }); + + if (user) { + user.followedOrganizations.push(new OrganizationSlug(slug)); + return user.save(); + } + return user; +}; + +/** + * Deletes organization slug from user's followedOrganizations. + */ +const unfollowOrganization = async (uuid: string, orgSlug: string): Promise<User> => { + const user = await UserModel.findOne({ uuid }); + if (user) { + const orgSlugs = user.followedOrganizations.map((orgSlugObj) => orgSlugObj.slug); + const orgIndex = orgSlugs.indexOf(orgSlug); + + if (orgIndex === -1) return user; + + user.followedOrganizations.splice(orgIndex, 1); + return user.save(); + } + return user; +}; + const getUserByUUID = async (uuid: string): Promise<User> => { return UserModel.findOne({ uuid }); }; @@ -79,41 +111,70 @@ const getUsersFollowingPublication = async (pubSlug: string): Promise<User[]> => return matchedUsers; }; +/** + * Return all users who follow an organization + */ +const getUsersFollowingOrganization = async (orgSlug: string): Promise<User[]> => { + const matchedUsers = await UserModel.find({ + followedOrganizations: { $elemMatch: { slug: orgSlug } }, + }); + return matchedUsers; +}; + /** * Add article to a user's readArticles */ const appendReadArticle = async (uuid: string, articleID: string): Promise<User> => { const user = await UserModel.findOne({ uuid }); - if (!user) return user; - - const article = await ArticleRepo.getArticleByID(articleID); - const checkDuplicates = (prev: boolean, cur: Article) => prev || cur.id === articleID; + if (user) { + const article = await ArticleRepo.getArticleByID(articleID); + const checkDuplicates = (prev: boolean, cur: Article) => prev || cur.id === articleID; if (article && !user.readArticles.reduce(checkDuplicates, false)) { user.readArticles.push(article); } - user.save(); + user.save(); + } return user; }; /** - * Add a magazine to a user's readMagazines + * Add a flyer to a user's readFlyers */ -const appendReadMagazine = async (uuid: string, magazineID: string): Promise<User> => { +const appendReadFlyer = async (uuid: string, flyerID: string): Promise<User> => { const user = await UserModel.findOne({ uuid }); - if (!user) return user; + if (user) { + const flyer = await FlyerRepo.getFlyerByID(flyerID); + const checkDuplicates = (prev: boolean, cur: Flyer) => prev || cur.id === flyerID; - const magazine = await MagazineRepo.getMagazineByID(magazineID); - const checkDuplicates = (prev: boolean, cur: Magazine) => prev || cur.id === magazineID; + if (flyer && !user.readFlyers.reduce(checkDuplicates, false)) { + user.readFlyers.push(flyer); + } - if (magazine && !user.readMagazines.reduce(checkDuplicates, false)) { - user.readMagazines.push(magazine); + user.save(); } + return user; +}; + +/** + * Add a magazine to a user's read + */ +const appendReadMagazine = async (uuid: string, magazineID: string): Promise<User> => { + const user = await UserModel.findOne({ uuid }); + + if (user) { + const magazine = await MagazineRepo.getMagazineByID(magazineID); + const checkDuplicates = (prev: boolean, cur: Magazine) => prev || cur.id === magazineID; - user.save(); + if (magazine && !user.readMagazines.reduce(checkDuplicates, false)) { + user.readMagazines.push(magazine); + } + + user.save(); + } return user; }; @@ -148,10 +209,14 @@ const incrementBookmarks = async (uuid: string): Promise<User> => { export default { appendReadArticle, appendReadMagazine, + appendReadFlyer, createUser, followPublication, + unfollowOrganization, + followOrganization, getUserByUUID, getUsersFollowingPublication, + getUsersFollowingOrganization, incrementBookmarks, incrementShoutouts, unfollowPublication, diff --git a/src/resolvers/OrganizationResolver.ts b/src/resolvers/OrganizationResolver.ts index 7943d91..19a520c 100644 --- a/src/resolvers/OrganizationResolver.ts +++ b/src/resolvers/OrganizationResolver.ts @@ -13,20 +13,20 @@ class OrganizationResolver { return OrganizationRepo.getAllOrganizations(); } - @Query((_returns) => Organization, { + @Query((_returns) => [Organization], { nullable: true, - description: 'Returns a single <Organization> via a given <id>', + description: 'Returns a list of <Organization>s via a given <categorySlug>', }) - async getOrganizationByID(@Arg('id') id: string) { - return OrganizationRepo.getOrganizationByID(id); + async getOrganizationsByCategory(@Arg('categorySlug') categorySlug: string) { + return OrganizationRepo.getOrganizationsByCategory(categorySlug); } @Query((_returns) => Organization, { nullable: true, - description: 'Returns a single <Organization> via a given <slug>', + description: 'Returns a single <Organization> via a given <id>', }) - async getOrganizationBySlug(@Arg('slug') slug: string) { - return OrganizationRepo.getOrganizationBySlug(slug); + async getOrganizationByID(@Arg('id') id: string) { + return OrganizationRepo.getOrganizationByID(id); } @Query((_returns) => [Organization], { @@ -36,26 +36,34 @@ class OrganizationResolver { return OrganizationRepo.getOrganizationsByIDs(ids); } + @Query((_returns) => Organization, { + nullable: true, + description: 'Returns a single <Organization> via a given <slug>', + }) + async getOrganizationBySlug(@Arg('slug') slug: string) { + return OrganizationRepo.getOrganizationBySlug(slug); + } + @FieldResolver((_returns) => Flyer, { nullable: true, - description: 'The most recent <Flyer> of an <Organization>', + description: 'Returns the most recent <Flyer> of an <Organization>', }) async mostRecentFlyer(@Root() organization: Organization): Promise<Flyer> { return OrganizationRepo.getMostRecentFlyer(organization); } @FieldResolver((_returns) => Number, { - description: "The total shoutouts of an <Organization's> <Flyers>", + description: 'Returns the total number of <Flyers> from an <Organization>', }) - async shoutouts(@Root() organization: Organization): Promise<number> { - return OrganizationRepo.getShoutouts(organization); + async numFlyers(@Root() organization: Organization): Promise<number> { + return OrganizationRepo.getNumFlyers(organization); } @FieldResolver((_returns) => Number, { - description: 'The total number of <Flyers> from an <Organization>', + description: "Returns the total shoutouts of an <Organization's> <Flyers>", }) - async numFlyers(@Root() organization: Organization): Promise<number> { - return OrganizationRepo.getNumFlyers(organization); + async shoutouts(@Root() organization: Organization): Promise<number> { + return OrganizationRepo.getShoutouts(organization); } } diff --git a/src/resolvers/UserResolver.ts b/src/resolvers/UserResolver.ts index 7b73f39..39f9253 100644 --- a/src/resolvers/UserResolver.ts +++ b/src/resolvers/UserResolver.ts @@ -26,6 +26,14 @@ class UserResolver { return await UserRepo.createUser(deviceToken, followedPublicationsSlugs, deviceType); } + @Mutation((_returns) => User, { + nullable: true, + description: 'User with id <uuid> follows the <Organization> referenced by <slug>', + }) + async followOrganization(@Arg('uuid') uuid: string, @Arg('slug') slug: string) { + return await UserRepo.followOrganization(uuid, slug); + } + @Mutation((_returns) => User, { nullable: true, description: 'User with id <uuid> follows the <Publication> referenced by <slug>', @@ -34,6 +42,14 @@ class UserResolver { return await UserRepo.followPublication(uuid, slug); } + @Mutation((_returns) => User, { + nullable: true, + description: 'User with id <uuid> unfollows the <Organization> referenced by <slug>', + }) + async unfollowOrganization(@Arg('uuid') uuid: string, @Arg('slug') slug: string) { + return await UserRepo.unfollowOrganization(uuid, slug); + } + @Mutation((_returns) => User, { nullable: true, description: 'User with id <uuid> unfollows the <Publication> referenced by <slug>', @@ -58,6 +74,14 @@ class UserResolver { return await UserRepo.appendReadMagazine(uuid, magazineID); } + @Mutation((_returns) => User, { + nullable: true, + description: "Adds the <Flyer> given by the <flyerID> to the <User's> read flyers", + }) + async readFlyer(@Arg('uuid') uuid: string, @Arg('flyerID') flyerID: string) { + return await UserRepo.appendReadFlyer(uuid, flyerID); + } + @Mutation((_returns) => User, { nullable: true, description: 'Increments the number of bookmarks for the <User> given by <uuid>', diff --git a/src/tests/data/FlyerFactory.ts b/src/tests/data/FlyerFactory.ts index 1c2b214..35f9780 100644 --- a/src/tests/data/FlyerFactory.ts +++ b/src/tests/data/FlyerFactory.ts @@ -51,8 +51,8 @@ class FlyerFactory { fakeFlyer.imageURL = faker.image.cats(); fakeFlyer.flyerURL = faker.datatype.string(); fakeFlyer.location = faker.datatype.string(); - fakeFlyer.organization = exampleOrg; - fakeFlyer.organizationSlug = exampleOrg.slug; + fakeFlyer.organizations = [exampleOrg]; + fakeFlyer.organizationSlugs = [exampleOrg.slug]; fakeFlyer.title = faker.commerce.productDescription(); fakeFlyer.isTrending = _.sample([true, false]); fakeFlyer.nsfw = _.sample([true, false]); diff --git a/src/tests/flyer.test.ts b/src/tests/flyer.test.ts index b58cc23..3cc4ed3 100644 --- a/src/tests/flyer.test.ts +++ b/src/tests/flyer.test.ts @@ -88,8 +88,8 @@ describe('getFlyersByOrganizationSlug(s) tests', () => { test('getFlyersByOrganizationSlug - 1 organization, 1 flyer', async () => { const org = await OrganizationFactory.getRandomOrganization(); const flyers = await FlyerFactory.createSpecific(1, { - organizationSlug: org.slug, - organization: org, + organizationSlugs: [org.slug], + organization: [org], }); await FlyerModel.insertMany(flyers); @@ -101,8 +101,8 @@ describe('getFlyersByOrganizationSlug(s) tests', () => { const org = await OrganizationFactory.getRandomOrganization(); const flyers = ( await FlyerFactory.createSpecific(3, { - organizationSlug: org.slug, - organization: org, + organizationSlugs: [org.slug], + organizations: [org], }) ).sort(FactoryUtils.compareByDate); @@ -114,16 +114,21 @@ describe('getFlyersByOrganizationSlug(s) tests', () => { ); }); - test('getFlyersByOrganizationSlugs - many organizations, 5 flyers', async () => { - const flyers = (await FlyerFactory.create(3)).sort(FactoryUtils.compareByDate); - + test('getFlyersByOrganizationSlugs - many organizations, 3 flyers', async () => { + const org1 = await OrganizationFactory.getRandomOrganization(); + const org2 = await OrganizationFactory.getRandomOrganization(); + const flyers = ( + await FlyerFactory.createSpecific(3, { + organizationSlugs: [org1.slug, org2.slug], + }) + ).sort(FactoryUtils.compareByDate); await FlyerModel.insertMany(flyers); - const getFlyersResponse = await FlyerRepo.getFlyersByOrganizationSlug( - FactoryUtils.mapToValue(flyers, 'organizationSlug'), - ); - expect(FactoryUtils.mapToValue(getFlyersResponse, 'title')).toEqual( - FactoryUtils.mapToValue(flyers, 'title'), + const getFlyersResponse1 = await FlyerRepo.getFlyersByOrganizationSlugs([org1.slug]); + const getFlyersResponse2 = await FlyerRepo.getFlyersByOrganizationSlugs([org2.slug]); + + expect(FactoryUtils.mapToValue(getFlyersResponse1, 'title')).toEqual( + FactoryUtils.mapToValue(getFlyersResponse2, 'title'), ); }); }); diff --git a/src/tests/organization.test.ts b/src/tests/organization.test.ts index 4694bf2..02d47e5 100644 --- a/src/tests/organization.test.ts +++ b/src/tests/organization.test.ts @@ -27,27 +27,27 @@ afterAll(async () => { describe('getAllOrganizations tests:', () => { test('getAllOrganizations', async () => { - const pubs = await OrganizationFactory.getAllOrganizations(); + const orgs = await OrganizationFactory.getAllOrganizations(); const getOrganizationsResponse = await OrganizationRepo.getAllOrganizations(); expect(FactoryUtils.mapToValue(getOrganizationsResponse, 'slug')).toEqual( - FactoryUtils.mapToValue(pubs, 'slug'), + FactoryUtils.mapToValue(orgs, 'slug'), ); }); }); describe('getMostRecentFlyer tests:', () => { test('getMostRecentFlyer - Out of 2-10 flyers', async () => { - const pub = await OrganizationRepo.getOrganizationBySlug( + const org = await OrganizationRepo.getOrganizationBySlug( (await OrganizationFactory.getRandomOrganization()).slug, ); const flyers = await FlyerFactory.createSpecific(_.random(2, 10), { - organizationSlug: pub.slug, - organization: pub, + organizationSlugs: [org.slug], + organizations: [org], }); await FlyerModel.insertMany(flyers); - const getOrganizationsResponse = await OrganizationRepo.getMostRecentFlyer(pub); + const getOrganizationsResponse = await OrganizationRepo.getMostRecentFlyer(org); const respDate = new Date(getOrganizationsResponse.date); const flyerDates = FactoryUtils.mapToValue(flyers, 'date'); @@ -62,56 +62,71 @@ describe('getMostRecentFlyer tests:', () => { describe('getNumFlyer tests:', () => { test('getNumFlyer - 0 flyers', async () => { - const pub = await OrganizationRepo.getOrganizationBySlug( + const org = await OrganizationRepo.getOrganizationBySlug( (await OrganizationFactory.getRandomOrganization()).slug, ); - const numResp = await OrganizationRepo.getNumFlyers(pub); + const numResp = await OrganizationRepo.getNumFlyers(org); expect(numResp).toEqual(0); }); test('getNumFlyer - Random number of flyers', async () => { - const pub = await OrganizationRepo.getOrganizationBySlug( + const org1 = await OrganizationRepo.getOrganizationBySlug( + (await OrganizationFactory.getRandomOrganization()).slug, + ); + const org2 = await OrganizationRepo.getOrganizationBySlug( (await OrganizationFactory.getRandomOrganization()).slug, ); const numFlyers = _.random(1, 10); const flyers = await FlyerFactory.createSpecific(numFlyers, { - organizationSlug: pub.slug, - organization: pub, + organizationSlugs: [org1.slug], + organizations: [org1, org2], }); await FlyerModel.insertMany(flyers); - const numResp = await OrganizationRepo.getNumFlyers(pub); + const numResp = await OrganizationRepo.getNumFlyers(org1); expect(numResp).toEqual(numFlyers); }); }); +describe('getOrganizationByCategory tests:', () => { + test('getOrganizationByCategory - 1 category', async () => { + const org = await OrganizationFactory.getRandomOrganization(); + const { categorySlug } = org; + + const getOrganizationsResponse = await OrganizationRepo.getOrganizationsByCategory( + categorySlug, + ); + expect(FactoryUtils.mapToValue(getOrganizationsResponse, 'slug')).toContain(org.slug); + }); +}); + describe('getOrganizationBySlug tests:', () => { - test('getOrganizationBySlug - 1 pub', async () => { - const pub = await OrganizationFactory.getRandomOrganization(); + test('getOrganizationBySlug - 1 org', async () => { + const org = await OrganizationFactory.getRandomOrganization(); - const getOrganizationsResponse = await OrganizationRepo.getOrganizationBySlug(pub.slug); + const getOrganizationsResponse = await OrganizationRepo.getOrganizationBySlug(org.slug); - expect(getOrganizationsResponse.slug).toEqual(pub.slug); + expect(getOrganizationsResponse.slug).toEqual(org.slug); }); }); describe('getShoutouts tests:', () => { - test('getShoutouts - Random number of flyers with 2 shoutouts, 1 pub', async () => { - const pub = await OrganizationRepo.getOrganizationBySlug( + test('getShoutouts - Random number of flyers with 2 shoutouts, 1 org', async () => { + const org = await OrganizationRepo.getOrganizationBySlug( (await OrganizationFactory.getRandomOrganization()).slug, ); const numFlyers = _.random(1, 20); const numShoutouts = numFlyers * 2; const flyers = await FlyerFactory.createSpecific(numFlyers, { - organizationSlug: pub.slug, - organization: pub, + organizationSlugs: [org.slug], + organizations: [org], shoutouts: 2, }); await FlyerModel.insertMany(flyers); - const getOrganizationsResponse = await OrganizationRepo.getShoutouts(pub); + const getOrganizationsResponse = await OrganizationRepo.getShoutouts(org); expect(getOrganizationsResponse).toEqual(numShoutouts); }); diff --git a/src/tests/user.test.ts b/src/tests/user.test.ts index 73179d3..cee5e8a 100644 --- a/src/tests/user.test.ts +++ b/src/tests/user.test.ts @@ -13,6 +13,9 @@ import { dbConnection, disconnectDB } from './data/TestingDBConnection'; import PublicationFactory from './data/PublicationFactory'; import MagazineFactory from './data/MagazineFactory'; import ArticleFactory from './data/ArticleFactory'; +import { FlyerModel } from '../entities/Flyer'; +import FlyerFactory from './data/FlyerFactory'; +import OrganizationFactory from './data/OrganizationFactory'; beforeAll(async () => { await dbConnection(); @@ -20,17 +23,20 @@ beforeAll(async () => { await MagazineModel.createCollection(); await UserModel.createCollection(); await ArticleModel.createCollection(); + await FlyerModel.createCollection(); }); beforeEach(async () => { await UserModel.deleteMany({}); await MagazineModel.deleteMany({}); await ArticleModel.deleteMany({}); + await FlyerModel.deleteMany({}); }); afterAll(async () => { UserModel.deleteMany({}); ArticleModel.deleteMany({}); + FlyerModel.deleteMany({}); MagazineModel.deleteMany({}).then(disconnectDB); }); @@ -95,6 +101,57 @@ describe('(un)followPublication tests:', () => { }); }); +describe('(un)followOrganization tests:', () => { + test('followOrganization - 1 user, 1 organization', async () => { + const organization = await OrganizationFactory.getRandomOrganization(); + const users = await UserFactory.create(1); + await UserModel.insertMany(users); + await UserRepo.followOrganization(users[0].uuid, organization.slug); + + const getUserResponse = await UserRepo.getUsersFollowingOrganization(organization.slug); + expect(FactoryUtils.mapToValue(getUserResponse, 'uuid')).toEqual( + FactoryUtils.mapToValue(users, 'uuid'), + ); + }); + + test('followOrganization - 3 users, 1 organization', async () => { + const organization = await OrganizationFactory.getRandomOrganization(); + const users = await UserFactory.create(3); + await UserModel.insertMany(users); + for (let i = 0; i < 3; i++) await UserRepo.followOrganization(users[i].uuid, organization.slug); + + const getUsersResponse = await UserRepo.getUsersFollowingOrganization(organization.slug); + expect(FactoryUtils.mapToValue(getUsersResponse, 'uuid')).toEqual( + FactoryUtils.mapToValue(users, 'uuid'), + ); + }); + + test('unfollowOrganization - 1 user, 1 organization', async () => { + const organization = await OrganizationFactory.getRandomOrganization(); + const users = await UserFactory.create(1); + await UserModel.insertMany(users); + + await UserRepo.followOrganization(users[0].uuid, organization.slug); + await UserRepo.unfollowOrganization(users[0].uuid, organization.slug); + + const getUsersResponse = await UserRepo.getUsersFollowingOrganization(organization.slug); + expect(getUsersResponse).toHaveLength(0); + }); + + test('unfollowOrganization - 3 users, 1 organization', async () => { + const organization = await OrganizationFactory.getRandomOrganization(); + const users = await UserFactory.create(3); + await UserModel.insertMany(users); + + for (let i = 0; i < 3; i++) await UserRepo.followOrganization(users[i].uuid, organization.slug); + for (let i = 0; i < 2; i++) + await UserRepo.unfollowOrganization(users[i].uuid, organization.slug); + + const getUsersResponse = await UserRepo.getUsersFollowingOrganization(organization.slug); + expect(getUsersResponse).toHaveLength(1); + }); +}); + // testing with user.test.ts instead of a separate weekly debrief file because // mutations are done with UserRepo, also because testing WeeklyDebrief requires // mocking which isn't a priority as of right now @@ -125,6 +182,21 @@ describe('weekly debrief tests:', () => { expect(getUsersResponse.numBookmarkedArticles).toEqual(1); }); + test('appendReadFlyer', async () => { + const users = await UserFactory.create(1); + const flyers = await FlyerFactory.create(1); + await UserModel.insertMany(users); + const insertOutput = await FlyerModel.insertMany(flyers); + await UserRepo.appendReadFlyer(users[0].uuid, insertOutput[0].id); + + // update database + const pub = await PublicationFactory.getRandomPublication(); + await UserRepo.followPublication(users[0].uuid, pub.slug); + + const getUsersResponse = await UserRepo.getUserByUUID(users[0].uuid); + expect(getUsersResponse.readFlyers).toHaveLength(1); + }); + test('appendReadMagazine', async () => { const users = await UserFactory.create(1); const magazines = await MagazineFactory.create(1);