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

Built out shift signup endpoints #90

Merged
merged 23 commits into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7a0eca1
added graphql shift signup types
hujoseph99 Jan 16, 2022
b1ef918
added shift signup interface
hujoseph99 Jan 16, 2022
ddbe06b
added note field to signup prisma model
hujoseph99 Jan 16, 2022
2312ea5
added shift signup types
hujoseph99 Jan 16, 2022
718cf5f
built out create shift signups endpoint
hujoseph99 Jan 16, 2022
3377254
added migration for foreign key and unique constraints for signups
hujoseph99 Jan 16, 2022
42ff948
built out update endpoint
hujoseph99 Jan 16, 2022
5b3e3d9
built out getshiftsignups endpoint
hujoseph99 Jan 16, 2022
ab2e68c
reverted middlewares and added some for new endpoints
hujoseph99 Jan 16, 2022
027949f
fixed linting errors
hujoseph99 Jan 16, 2022
fc9f0cc
fixed linting error
hujoseph99 Jan 16, 2022
486aeae
corrected mutation typo
hujoseph99 Jan 25, 2022
a720ed4
Merge branch 'main' into create-signup-service
hujoseph99 Jan 25, 2022
cf4d19d
update migrations with main branch
hujoseph99 Jan 29, 2022
c5a9874
update createshiftsignup middleware and updates for change of pk for …
hujoseph99 Jan 29, 2022
9f0e3bf
linting fixes
hujoseph99 Jan 29, 2022
d57e636
add userid check to authorizedByVolunteer middleware
hujoseph99 Jan 30, 2022
6095a1a
fix linting error
hujoseph99 Jan 30, 2022
d0da7d9
Merge branch 'main' into create-signup-service
hujoseph99 Feb 3, 2022
8d3412f
update error message to use utils
hujoseph99 Feb 3, 2022
d17593e
update migrations to be up to date with main
hujoseph99 Feb 3, 2022
50429f1
added missing type to error param
hujoseph99 Feb 3, 2022
7fcbc99
remove unnecessary optional chaining + middleware
hujoseph99 Feb 5, 2022
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
8 changes: 8 additions & 0 deletions backend/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import userResolvers from "./resolvers/userResolvers";
import userType from "./types/userType";
import shiftResolvers from "./resolvers/shiftResolvers";
import shiftType from "./types/shiftType";
import shiftSignupResolvers from "./resolvers/shiftSignupResolvers";
import shiftSignupType from "./types/shiftSignupType";
import postingResolvers from "./resolvers/postingResolvers";
import postingType from "./types/postingType";
import skillResolvers from "./resolvers/skillResolvers";
Expand Down Expand Up @@ -105,6 +107,7 @@ const executableSchema = makeExecutableSchema({
entityType,
userType,
shiftType,
shiftSignupType,
skillType,
skillType,
postingType,
Expand All @@ -117,6 +120,7 @@ const executableSchema = makeExecutableSchema({
entityResolvers,
userResolvers,
shiftResolvers,
shiftSignupResolvers,
postingResolvers,
skillResolvers,
branchResolvers,
Expand All @@ -128,6 +132,7 @@ const authorizedByAllRoles = () =>
const authorizedByAdmin = () => isAuthorizedByRole(new Set(["ADMIN"]));
const authorizedByAdminAndVolunteer = () =>
isAuthorizedByRole(new Set(["ADMIN", "VOLUNTEER"]));
const authorizedByVolunteer = () => isAuthorizedByRole(new Set(["VOLUNTEER"]));
Copy link
Member

Choose a reason for hiding this comment

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

we also want to check isAuthorizedByUserId here to check that the userid in the token is the same as the userid they are signingup the shift as

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added!


const graphQLMiddlewares = {
Query: {
Expand All @@ -147,6 +152,7 @@ const graphQLMiddlewares = {
skills: authorizedByAdmin(),
branch: authorizedByAdmin(),
branches: authorizedByAdmin(),
getShiftSignupsForUser: authorizedByAdmin(),
},
Mutation: {
createEntity: authorizedByAllRoles(),
Expand Down Expand Up @@ -175,6 +181,8 @@ const graphQLMiddlewares = {
createBranch: authorizedByAdmin(),
updateBranch: authorizedByAdmin(),
deleteBranch: authorizedByAdmin(),
createShiftSignups: authorizedByVolunteer(),
updateShiftSignup: authorizedByAdmin(),
},
};

Expand Down
45 changes: 45 additions & 0 deletions backend/graphql/resolvers/shiftSignupResolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ShiftSignupService from "../../services/implementations/shiftSignupService";
import IShiftSignupService from "../../services/interfaces/shiftSignupService";
import {
CreateShiftSignupDTO,
ShiftSignupResponseDTO,
UpdateShiftSignupRequestDTO,
} from "../../types";

const shiftSignupService: IShiftSignupService = new ShiftSignupService();

const shiftSignupResolvers = {
Query: {
getShiftSignupsForUser: async (
_parent: undefined,
{ userId }: { userId: string },
): Promise<ShiftSignupResponseDTO[]> => {
return shiftSignupService.getShiftSignupsForUser(userId);
},
},
Mutation: {
createShiftSignups: async (
_parent: undefined,
{ shifts }: { shifts: CreateShiftSignupDTO[] },
): Promise<ShiftSignupResponseDTO[]> => {
return shiftSignupService.createShiftSignups(shifts);
},

updateShiftSignup: async (
_parent: undefined,
{
shiftId,
userId,
update,
}: {
shiftId: string;
userId: string;
update: UpdateShiftSignupRequestDTO;
},
): Promise<ShiftSignupResponseDTO> => {
return shiftSignupService.updateShiftSignup(shiftId, userId, update);
},
},
};

export default shiftSignupResolvers;
47 changes: 47 additions & 0 deletions backend/graphql/types/shiftSignupType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { gql } from "apollo-server-express";

const shiftSignupType = gql`
enum SignupStatus {
PENDING
CONFIRMED
CANCELED
}

input CreateShiftSignupRequestDTO {
shiftId: ID!
userId: ID!
numVolunteers: Int!
note: String!
}

input UpdateShiftSignupRequestDTO {
numVolunteers: Int!
note: String!
status: SignupStatus!
}

type ShiftSignupResponseDTO {
shiftId: ID!
userId: ID!
numVolunteers: Int!
note: String!
status: SignupStatus!
}

extend type Query {
getShiftSignupsForUser(userId: ID!): [ShiftSignupResponseDTO!]!
}

extend type Mutation {
createShiftSignups(
shifts: [CreateShiftSignupRequestDTO!]!
): [ShiftSignupResponseDTO!]!
updateShiftSignup(
shiftId: ID!
userId: ID!
update: UpdateShiftSignupRequestDTO!
): ShiftSignupResponseDTO!
}
`;

export default shiftSignupType;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
Warnings:

- The primary key for the `signups` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `signups` table. All the data in the column will be lost.
- Added the required column `note` to the `signups` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "signups" DROP CONSTRAINT "signups_pkey",
DROP COLUMN "id",
ADD COLUMN "note" TEXT NOT NULL,
ADD CONSTRAINT "signups_pkey" PRIMARY KEY ("shifts_id", "userId");
3 changes: 2 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,16 @@ model Shift {
}

model Signup {
id Int @id @default(autoincrement())
shift Shift @relation(fields: [shiftId], references: [id])
shiftId Int @map("shifts_id")
user User @relation(fields: [userId], references: [id])
Copy link
Member

Choose a reason for hiding this comment

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

Another thought: we can just make the foreign key reference on the volunteers table rather than the base users table to enforce the "only volunteers can sign up" rule

Copy link
Member

Choose a reason for hiding this comment

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

That also works but I think our middleware for checking the role is good enough for now. We would still need to go through each shift to check that the volunteerID is the same as the volunteerID associated with the user in the token.

userId Int
status SignupStatus
numVolunteers Int @default(1) @map("num_volunteers")
note String

@@map("signups")
@@id([shiftId, userId])
}

model Volunteer {
Expand Down
94 changes: 94 additions & 0 deletions backend/services/implementations/shiftSignupService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { PrismaClient, Signup, SignupStatus } from "@prisma/client";

import IShiftSignupService from "../interfaces/shiftSignupService";
import {
CreateShiftSignupDTO,
ShiftSignupDTO,
ShiftSignupResponseDTO,
UpdateShiftSignupRequestDTO,
} from "../../types";
import logger from "../../utilities/logger";
import { getErrorMessage } from "../../utilities/errorUtils";

const prisma = new PrismaClient();

const Logger = logger(__filename);

class ShiftSignupService implements IShiftSignupService {
convertSignupToDTO = (signup: Signup): ShiftSignupDTO => {
return {
...signup,
shiftId: String(signup.shiftId),
userId: String(signup.userId),
};
};

async getShiftSignupsForUser(
userId: string,
): Promise<ShiftSignupResponseDTO[]> {
try {
const shiftSignups = await prisma.signup.findMany({
where: { userId: Number(userId) },
});
return shiftSignups.map((signup) => this.convertSignupToDTO(signup));
} catch (error: unknown) {
Logger.error(
`Failed to shift signups. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
}

async createShiftSignups(
hujoseph99 marked this conversation as resolved.
Show resolved Hide resolved
shiftSignups: CreateShiftSignupDTO[],
): Promise<ShiftSignupResponseDTO[]> {
try {
const newShiftSignups = await prisma.$transaction(
shiftSignups.map((shiftSignup) =>
prisma.signup.create({
data: {
...shiftSignup,
shiftId: Number(shiftSignup.shiftId),
userId: Number(shiftSignup.userId),
status: SignupStatus.PENDING,
},
}),
),
);
return newShiftSignups.map((newShiftSignup) =>
this.convertSignupToDTO(newShiftSignup),
);
} catch (error: unknown) {
Logger.error(
`Failed to create shift signup. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
}

async updateShiftSignup(
shiftId: string,
userId: string,
shiftSignup: UpdateShiftSignupRequestDTO,
): Promise<ShiftSignupResponseDTO> {
try {
const updateResult = await prisma.signup.update({
where: {
shiftId_userId: {
shiftId: Number(shiftId),
userId: Number(userId),
},
},
data: shiftSignup,
});
return this.convertSignupToDTO(updateResult);
} catch (error: unknown) {
Logger.error(
`Failed to update shift signup. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
}
}

export default ShiftSignupService;
41 changes: 41 additions & 0 deletions backend/services/interfaces/shiftSignupService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
CreateShiftSignupDTO,
ShiftSignupResponseDTO,
UpdateShiftSignupRequestDTO,
} from "../../types";

interface IShiftSignupService {
/**
* Bulk creates sign ups for shifts
* @param shiftSignups array of CreateShiftSignupDTOs
* @returns an array of ShiftSignupResponseDTO
* @throws Error if any of the shifts signups cannot be created
*/
createShiftSignups(
shiftSignups: CreateShiftSignupDTO[],
): Promise<ShiftSignupResponseDTO[]>;

/**
* Update a shift signup entry
* @param shiftId the shift to update
* @param userId the user to update
* @param shiftSignup the information to be updated
* @returns a ShiftSignupResponseDTO containing the updated information
* @throws Error if the shift signup
*/
updateShiftSignup(
shiftId: string,
userId: string,
shiftSignup: UpdateShiftSignupRequestDTO,
): Promise<ShiftSignupResponseDTO>;

/**
* Gets all shifts the user has signed up for
* @param userId the target user's id
* @returns an array of ShiftSignupResponseDTOs for each shift the user signed up for
* @throws Error if the shift signup retrieval fails
*/
getShiftSignupsForUser(userId: string): Promise<ShiftSignupResponseDTO[]>;
}

export default IShiftSignupService;
19 changes: 19 additions & 0 deletions backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ export type ShiftBulkRequestDTO = {

export type ShiftResponseDTO = ShiftDTO;

export type ShiftSignupStatus = "PENDING" | "CONFIRMED" | "CANCELED";

export type ShiftSignupDTO = {
shiftId: string;
userId: string;
numVolunteers: number;
note: string;
status: ShiftSignupStatus;
};

export type CreateShiftSignupDTO = Omit<ShiftSignupDTO, "status">;

export type UpdateShiftSignupRequestDTO = Omit<
ShiftSignupDTO,
"shiftId" | "userId"
>;

export type ShiftSignupResponseDTO = ShiftSignupDTO;

export type SkillDTO = {
id: string;
name: string;
Expand Down