Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement community board (user queries/mutations) [5/7] #87

Merged
merged 28 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c541920
Kidus/filter articles (#48)
kidzegeye Feb 25, 2022
6ac7a63
Fix BRSN's url
kidzegeye Sep 14, 2022
e3601a2
Migrate bookmark resolver and update User Repo & Entity
Archit404Error Sep 20, 2022
d38532a
Merge main into release
Archit404Error Sep 24, 2022
9d90039
Resolve remaining merge conflicts
Archit404Error Sep 25, 2022
14a4ab8
Implement chronological sorting
Archit404Error Sep 25, 2022
7cc3f3a
Merge branch 'archit/chrono-order' into release
Archit404Error Sep 25, 2022
38aa425
Merge slug changes into release
Archit404Error Oct 22, 2022
fd106f6
Update prod deployment script
Archit404Error Oct 30, 2022
6bf0a2f
Merge branch 'main' into release
Archit404Error Oct 31, 2022
dbc08d3
Merge branch 'main' into release
Archit404Error Nov 2, 2022
1e1c323
Remove unused MagazineURL index from Magazine DB
kidzegeye Nov 2, 2022
b8f6c8b
Merge branch 'main' into release
Archit404Error Nov 4, 2022
f257b73
Add no rules to publications.json
Archit404Error Nov 10, 2022
de73d2f
Merge branch 'archit/add-no-rules' into release
Archit404Error Nov 10, 2022
861e796
Remove trailing comma
Archit404Error Nov 10, 2022
c898bef
Begin implementing FlyerRepo
isaachan100 Mar 20, 2023
74f743e
Merge branch 'main' of github.com:cuappdev/volume-backend into isaac/…
isaachan100 Mar 21, 2023
88ce176
Implement basic community board features
isaachan100 Mar 25, 2023
6e45574
Merge branch 'main' of github.com:cuappdev/volume-backend into isaac/…
isaachan100 Mar 25, 2023
95e109d
Remove unnecessary OrganizationRepo function
isaachan100 Mar 25, 2023
e133b65
Merge branch 'main' of github.com:cuappdev/volume-backend into isaac/…
isaachan100 Mar 25, 2023
c4401cc
Implement user routes for community board
isaachan100 Mar 29, 2023
d2b12f6
Merge branch 'release' of github.com:cuappdev/volume-backend into isa…
isaachan100 Apr 11, 2023
fa00457
Merge branch 'main' into isaac/community-board-05
isaachan100 Apr 28, 2023
90957cd
Implement mutations for following and unfollowing organizations
isaachan100 Apr 28, 2023
1e989af
Address pr review comments
isaachan100 Apr 28, 2023
d12a835
Address pr review comments
isaachan100 Apr 28, 2023
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
9 changes: 6 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ 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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love that we appdev members know the alphabetical order


const main = async () => {
const schema = await buildSchema({
Expand Down Expand Up @@ -76,13 +77,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();
});
}

Expand Down
18 changes: 18 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
97 changes: 81 additions & 16 deletions src/repos/UserRepo.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -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();
}
Comment on lines +76 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this if statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it might return null otherwise

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();
}
Comment on lines +88 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be checking if user is null or not

return user;
};

const getUserByUUID = async (uuid: string): Promise<User> => {
return UserModel.findOne({ uuid });
};
Expand All @@ -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;
if (user) {
const article = await ArticleRepo.getArticleByID(articleID);
const checkDuplicates = (prev: boolean, cur: Article) => prev || cur.id === articleID;

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);
}

if (article && !user.readArticles.reduce(checkDuplicates, false)) {
user.readArticles.push(article);
user.save();
}
return user;
};

/**
* Add a flyer to a user's readFlyers
*/
const appendReadFlyer = async (uuid: string, flyerID: string): Promise<User> => {
const user = await UserModel.findOne({ uuid });

if (user) {
const flyer = await FlyerRepo.getFlyerByID(flyerID);
const checkDuplicates = (prev: boolean, cur: Flyer) => prev || cur.id === flyerID;

if (flyer && !user.readFlyers.reduce(checkDuplicates, false)) {
user.readFlyers.push(flyer);
}

user.save();
user.save();
}
return user;
};

/**
* Add a magazine to a user's readMagazines
* 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) return user;
if (user) {
const magazine = await MagazineRepo.getMagazineByID(magazineID);
const checkDuplicates = (prev: boolean, cur: Magazine) => prev || cur.id === magazineID;

const magazine = await MagazineRepo.getMagazineByID(magazineID);
const checkDuplicates = (prev: boolean, cur: Magazine) => prev || cur.id === magazineID;
if (magazine && !user.readMagazines.reduce(checkDuplicates, false)) {
user.readMagazines.push(magazine);
}

if (magazine && !user.readMagazines.reduce(checkDuplicates, false)) {
user.readMagazines.push(magazine);
user.save();
}

user.save();
return user;
};

Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions src/resolvers/UserResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>',
Expand All @@ -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>',
Expand All @@ -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>',
Expand Down
72 changes: 72 additions & 0 deletions src/tests/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,30 @@ 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();
await PublicationRepo.addPublicationsToDB();
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);
});

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down