Skip to content

Commit

Permalink
Set Up Coffee Chat API (#635)
Browse files Browse the repository at this point in the history
* initial commit

* testing locally attempt

* change localhost:3000 to backendURL

* locally tested coffee chat api with buttons on shoutouts pg

* rm commented out code

* rm print statements, clean up

* fix github actions checks

* revert next-env.d.ts

* revert ShoutoutForm.tsx

* rm comment in api.ts

* Add documentation

* Run formatter

* Use deleteCollection to clear all coffee chats

* Only allow leads/admins to delete a coffee chat

---------

Co-authored-by: Alyssa Zhang <alyssayzhang@gmail.com>
Co-authored-by: Andrew Chen <andrew032012@gmail.com>
Co-authored-by: alyssayzhang <69472409+alyssayzhang@users.noreply.github.com>
  • Loading branch information
4 people authored Sep 22, 2024
1 parent 85416e1 commit 22d340d
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 0 deletions.
105 changes: 105 additions & 0 deletions backend/src/API/coffeeChatAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import isEqual from 'lodash.isequal';
import CoffeeChatDao from '../dao/CoffeeChatDao';
import PermissionsManager from '../utils/permissionsManager';
import { PermissionError } from '../utils/errors';

const coffeeChatDao = new CoffeeChatDao();

/**
* Gets all coffee chats
*/
export const getAllCoffeeChats = (): Promise<CoffeeChat[]> => coffeeChatDao.getAllCoffeeChats();

/**
* Gets all coffee chats for a user
* @param user - user whose coffee chats should be fetched
*/
export const getCoffeeChatsByUser = async (user: IdolMember): Promise<CoffeeChat[]> =>
coffeeChatDao.getCoffeeChatsByUser(user);

/**
* Creates a new coffee chat for member
* @param coffeeChat - Newly created CoffeeChat object
* A member can not coffee chat themselves.
* A member can not coffee chat the same person from previous semesters.
*/
export const createCoffeeChat = async (coffeeChat: CoffeeChat): Promise<CoffeeChat> => {
const [member1, member2] = coffeeChat.members;

if (isEqual(member1, member2)) {
throw new Error('Cannot create coffee chat with yourself.');
}

const prevChats1 = await coffeeChatDao.getCoffeeChatsByUser(member1);
const prevChats2 = await coffeeChatDao.getCoffeeChatsByUser(member2);

const prevChats = [...prevChats1, ...prevChats2];

const chatExists = prevChats.some((chat) => {
const chatMembers = chat.members.map((m) => m.email).sort();
const currentMembers = [member1.email, member2.email].sort();
return isEqual(chatMembers, currentMembers);
});

if (chatExists) {
throw new Error(
'Cannot create coffee chat with member. Previous coffee chats from previous semesters exist.'
);
}

await coffeeChatDao.createCoffeeChat(coffeeChat);
return coffeeChat;
};

/**
* Updates a coffee chat (if the user has permission)
* @param coffeeChat - The updated CoffeeChat object
* @param user - The user that is requesting to update the coffee chat
*/
export const updateCoffeeChat = async (
coffeeChat: CoffeeChat,
user: IdolMember
): Promise<CoffeeChat> => {
const canEditCoffeeChat = await PermissionsManager.canEditCoffeeChat(user);
if (!canEditCoffeeChat) {
throw new PermissionError(
`User with email ${user.email} does not have permissions to update coffee chat.`
);
}

await coffeeChatDao.updateCoffeeChat(coffeeChat);
return coffeeChat;
};

/**
* Deletes a coffee chat (if the user has permission)
* @param uuid - DB uuid of CoffeeChat
* @param user - The user that is requesting to delete a coffee chat
*/
export const deleteCoffeeChat = async (uuid: string, user: IdolMember): Promise<void> => {
const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user);
const coffeeChat = await coffeeChatDao.getCoffeeChat(uuid);

if (!coffeeChat) return;

if (!isLeadOrAdmin) {
throw new PermissionError(
`User with email ${user.email} does not have sufficient permissions to delete coffee chat.`
);
}
await coffeeChatDao.deleteCoffeeChat(uuid);
};

/**
* Deletes all coffee chats (if the user has permission)
* @param user - The user that is requesting to delete all coffee chats
*/
export const clearAllCoffeeChats = async (user: IdolMember): Promise<void> => {
const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user);
if (!isLeadOrAdmin) {
throw new PermissionError(
`User with email ${user.email} does not have sufficient permissions to delete all coffee chats.`
);
}
await CoffeeChatDao.clearAllCoffeeChats();
};
35 changes: 35 additions & 0 deletions backend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ import {
hideShoutout,
deleteShoutout
} from './API/shoutoutAPI';
import {
createCoffeeChat,
getAllCoffeeChats,
updateCoffeeChat,
getCoffeeChatsByUser,
deleteCoffeeChat,
clearAllCoffeeChats
} from './API/coffeeChatAPI';
import {
allSignInForms,
createSignInForm,
Expand Down Expand Up @@ -280,6 +288,33 @@ loginCheckedDelete('/shoutout/:uuid', async (req, user) => {
return {};
});

loginCheckedGet('/coffee-chat', async () => ({
coffeeChats: await getAllCoffeeChats()
}));

loginCheckedPost('/coffee-chat', async (req) => ({
coffeeChats: await createCoffeeChat(req.body)
}));

loginCheckedDelete('/coffee-chat', async (_, user) => {
await clearAllCoffeeChats(user);
return {};
});

loginCheckedDelete('/coffee-chat/:uuid', async (req, user) => {
await deleteCoffeeChat(req.params.uuid, user);
return {};
});

loginCheckedGet('/coffee-chat/:email', async (_, user) => {
const coffeeChats = await getCoffeeChatsByUser(user);
return { coffeeChats };
});

loginCheckedPut('/coffee-chat', async (req, user) => ({
coffeeChats: await updateCoffeeChat(req.body, user)
}));

// Pull from IDOL
loginCheckedPost('/pullIDOLChanges', (_, user) => requestIDOLPullDispatch(user));
loginCheckedGet('/getIDOLChangesPR', (_, user) => getIDOLChangesPR(user));
Expand Down
99 changes: 99 additions & 0 deletions backend/src/dao/CoffeeChatDao.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { v4 as uuidv4 } from 'uuid';
import { memberCollection, coffeeChatsCollection, db } from '../firebase';
import { DBCoffeeChat } from '../types/DataTypes';
import { getMemberFromDocumentReference } from '../utils/memberUtil';
import BaseDao from './BaseDao';
import { deleteCollection } from '../utils/firebase-utils';

async function materializeCoffeeChat(dbCoffeeChat: DBCoffeeChat): Promise<CoffeeChat> {
const member1 = await getMemberFromDocumentReference(dbCoffeeChat.members[0]);
const member2 = await getMemberFromDocumentReference(dbCoffeeChat.members[1]);

return {
...dbCoffeeChat,
members: [member1, member2]
};
}

async function serializeCoffeeChat(coffeeChat: CoffeeChat): Promise<DBCoffeeChat> {
const member1Data = memberCollection.doc(coffeeChat.members[0].email);
const member2Data = memberCollection.doc(coffeeChat.members[1].email);

return {
...coffeeChat,
members: [member1Data, member2Data]
};
}

export default class CoffeeChatDao extends BaseDao<CoffeeChat, DBCoffeeChat> {
constructor() {
super(coffeeChatsCollection, materializeCoffeeChat, serializeCoffeeChat);
}

/**
* Creates a new coffee chat for member
* @param coffeeChat - Newly created CoffeeChat object.
* If provided, the object uuid will be used. If not, a new one will be generated.
* The pending field will be set to true by default.
*/
async createCoffeeChat(coffeeChat: CoffeeChat): Promise<CoffeeChat> {
const coffeeChatWithUUID = {
...coffeeChat,
status: 'pending' as Status,
uuid: coffeeChat.uuid ? coffeeChat.uuid : uuidv4()
};
return this.createDocument(coffeeChatWithUUID.uuid, coffeeChatWithUUID);
}

/**
* Gets the coffee chats
* @param uuid - DB uuid of coffee chat
*/
async getCoffeeChat(uuid: string): Promise<CoffeeChat | null> {
return this.getDocument(uuid);
}

/**
* Gets all coffee chats
*/
async getAllCoffeeChats(): Promise<CoffeeChat[]> {
return this.getDocuments();
}

/**
* Updates a coffee chat
* @param coffeeChat - updated Coffee Chat object
*/
async updateCoffeeChat(coffeeChat: CoffeeChat): Promise<CoffeeChat> {
return this.updateDocument(coffeeChat.uuid, coffeeChat);
}

/**
* Gets all coffee chat for a user
* @param user - user whose coffee chats should be fetched
*/
async getCoffeeChatsByUser(user: IdolMember): Promise<CoffeeChat[]> {
return this.getDocuments([
{
field: 'members',
comparisonOperator: 'array-contains',
value: memberCollection.doc(user.email)
}
]);
}

/**
* Deletes a coffee chat
* @param uuid - DB uuid of CoffeeChat
*/
async deleteCoffeeChat(uuid: string): Promise<void> {
await this.deleteDocument(uuid);
}

/**
* Deletes all coffee chats for all users
*/
static async clearAllCoffeeChats(): Promise<void> {
await deleteCollection(db, 'coffee-chats', 500);
}
}
4 changes: 4 additions & 0 deletions backend/src/utils/permissionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default class PermissionsManager {
return this.isLeadOrAdmin(mem);
}

static async canEditCoffeeChat(mem: IdolMember): Promise<boolean> {
return this.isLeadOrAdmin(mem);
}

static async canEditDevPortfolio(mem: IdolMember): Promise<boolean> {
return this.isLeadOrAdmin(mem);
}
Expand Down

0 comments on commit 22d340d

Please sign in to comment.