Skip to content

Commit fc18747

Browse files
authored
Merge pull request #2114 from Northeastern-Electric-Racing/#2011-delete-design-review-endpoint
#2011 delete design review endpoint
2 parents d332dd4 + fc3327f commit fc18747

11 files changed

Lines changed: 266 additions & 3 deletions

File tree

src/backend/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import changeRequestsRouter from './src/routes/change-requests.routes';
1111
import descriptionBulletsRouter from './src/routes/description-bullets.routes';
1212
import tasksRouter from './src/routes/tasks.routes';
1313
import reimbursementRequestsRouter from './src/routes/reimbursement-requests.routes';
14+
import designReviewRouter from './src/routes/design-review.routes';
1415

1516
const app = express();
1617
const port = process.env.PORT || 3001;
@@ -52,6 +53,7 @@ app.use('/change-requests', changeRequestsRouter);
5253
app.use('/description-bullets', descriptionBulletsRouter);
5354
app.use('/tasks', tasksRouter);
5455
app.use('/reimbursement-requests', reimbursementRequestsRouter);
56+
app.use('/design-reviews', designReviewRouter);
5557
app.use('/', (_req, res) => {
5658
res.json('Welcome to FinishLine');
5759
});
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
1-
export default class DesignReviewController {}
1+
import { User } from '@prisma/client';
2+
import { Request, Response, NextFunction } from 'express';
3+
import { getCurrentUser } from '../utils/auth.utils';
4+
import DesignReviewService from '../services/design-review.services';
5+
6+
export default class DesignReviewController {
7+
static async deleteDesignReview(req: Request, res: Response, next: NextFunction) {
8+
try {
9+
const drId: string = req.params.designReviewId;
10+
const user: User = await getCurrentUser(res);
11+
const deletedDesignReview = await DesignReviewService.deleteDesignReview(user, drId);
12+
return res.status(200).json(deletedDesignReview);
13+
} catch (error: unknown) {
14+
next(error);
15+
}
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Prisma } from '@prisma/client';
2+
3+
const designReviewQueryArgs = Prisma.validator<Prisma.Design_ReviewArgs>()({
4+
include: {
5+
userCreated: true,
6+
teamType: true,
7+
requiredMembers: true,
8+
optionalMembers: true,
9+
confirmedMembers: true,
10+
deniedMembers: true,
11+
attendees: true,
12+
userDeleted: true,
13+
wbsElement: true
14+
}
15+
});
16+
17+
export default designReviewQueryArgs;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import express from 'express';
2+
import DesignReviewController from '../controllers/design-review.controllers';
23

34
const designReviewRouter = express.Router();
45

6+
designReviewRouter.delete('/:designReviewId/delete', DesignReviewController.deleteDesignReview);
7+
58
export default designReviewRouter;
Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,35 @@
1-
export default class DesignReviewService {}
1+
import { DesignReview, isAdmin } from 'shared';
2+
import prisma from '../prisma/prisma';
3+
import { AccessDeniedAdminOnlyException, DeletedException, NotFoundException } from '../utils/errors.utils';
4+
import { User } from '@prisma/client';
5+
import designReviewQueryArgs from '../prisma-query-args/design-review.query-args';
6+
import { designReviewTransformer } from '../transformers/design-review.transformer';
7+
8+
export default class DesignReviewService {
9+
/**
10+
* Deletes a design review
11+
* @param submitter the user who deleted the design review
12+
* @param designReviewId the id of the design review to be deleted
13+
*/
14+
15+
static async deleteDesignReview(submitter: User, designReviewId: string): Promise<DesignReview> {
16+
const designReview = await prisma.design_Review.findUnique({
17+
where: { designReviewId }
18+
});
19+
20+
if (!designReview) throw new NotFoundException('Design Review', designReviewId);
21+
22+
if (!(isAdmin(submitter.role) || submitter.userId === designReview.userCreatedId))
23+
throw new AccessDeniedAdminOnlyException('delete design reviews');
24+
25+
if (designReview.dateDeleted) throw new DeletedException('Design Review', designReviewId);
26+
27+
const deletedDesignReview = await prisma.design_Review.update({
28+
where: { designReviewId },
29+
data: { dateDeleted: new Date(), userDeleted: { connect: { userId: submitter.userId } } },
30+
...designReviewQueryArgs
31+
});
32+
33+
return designReviewTransformer(deletedDesignReview);
34+
}
35+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Prisma } from '@prisma/client';
2+
import { DesignReview, DesignReviewStatus } from 'shared';
3+
import userTransformer from './user.transformer';
4+
import designReviewQueryArgs from '../prisma-query-args/design-review.query-args';
5+
import { wbsNumOf } from '../utils/utils';
6+
7+
export const designReviewTransformer = (
8+
designReview: Prisma.Design_ReviewGetPayload<typeof designReviewQueryArgs>
9+
): DesignReview => {
10+
return {
11+
designReviewId: designReview.designReviewId,
12+
dateScheduled: designReview.dateScheduled,
13+
meetingTimes: designReview.meetingTimes,
14+
dateCreated: designReview.dateCreated,
15+
userCreated: userTransformer(designReview.userCreated),
16+
requiredMembers: designReview.requiredMembers.map(userTransformer),
17+
optionalMembers: designReview.optionalMembers.map(userTransformer),
18+
confirmedMembers: designReview.confirmedMembers.map(userTransformer),
19+
deniedMembers: designReview.deniedMembers.map(userTransformer),
20+
location: designReview.location ?? undefined,
21+
isOnline: designReview.isOnline,
22+
isInPerson: designReview.isInPerson,
23+
zoomLink: designReview.zoomLink ?? undefined,
24+
attendees: designReview.attendees.map(userTransformer),
25+
dateDeleted: designReview.dateDeleted ?? undefined,
26+
userDeleted: designReview.userDeleted ? userTransformer(designReview.userDeleted) : undefined,
27+
docTemplateLink: designReview.docTemplateLink ?? undefined,
28+
status: designReview.status as DesignReviewStatus,
29+
teamType: designReview.teamType,
30+
wbsName: designReview.wbsElement.name,
31+
wbsNum: wbsNumOf(designReview.wbsElement)
32+
};
33+
};

src/backend/src/utils/errors.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,5 @@ type ExceptionObjectNames =
120120
| 'Manufacturer'
121121
| 'Unit'
122122
| 'Material'
123-
| 'Link Type';
123+
| 'Link Type'
124+
| 'Design Review';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { designReview1, prismaDesignReview2 } from './test-data/design-reviews.test-data';
2+
import { batman, wonderwoman } from './test-data/users.test-data';
3+
import DesignReviewService from '../src/services/design-review.services';
4+
import prisma from '../src/prisma/prisma';
5+
import { AccessDeniedAdminOnlyException, DeletedException, NotFoundException } from '../src/utils/errors.utils';
6+
7+
describe('Design Reviews', () => {
8+
beforeEach(() => {});
9+
10+
afterEach(() => {
11+
vi.clearAllMocks();
12+
});
13+
14+
describe('Delete Design Review Tests', () => {
15+
test('Delete Reimbursment Request fails when ID does not exist', async () => {
16+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(null);
17+
await expect(() => DesignReviewService.deleteDesignReview(batman, designReview1.designReviewId)).rejects.toThrow(
18+
new NotFoundException('Design Review', designReview1.designReviewId)
19+
);
20+
});
21+
test('Delete Design Review fails when user is not an admin nor the user who created the design review', async () => {
22+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(designReview1);
23+
await expect(() => DesignReviewService.deleteDesignReview(wonderwoman, designReview1.designReviewId)).rejects.toThrow(
24+
new AccessDeniedAdminOnlyException('delete design reviews')
25+
);
26+
});
27+
test('Delete Design Review fails when design review is already deleted', async () => {
28+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue({ ...designReview1, dateDeleted: new Date() });
29+
await expect(() => DesignReviewService.deleteDesignReview(batman, designReview1.designReviewId)).rejects.toThrow(
30+
new DeletedException('Design Review', designReview1.designReviewId)
31+
);
32+
});
33+
test('Delete Design Review succeeds when user is admin', async () => {
34+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview2);
35+
vi.spyOn(prisma.design_Review, 'update').mockResolvedValue(prismaDesignReview2);
36+
37+
expect(prismaDesignReview2.dateDeleted).toBeNull();
38+
39+
await DesignReviewService.deleteDesignReview(batman, prismaDesignReview2.designReviewId);
40+
41+
expect(prisma.design_Review.findUnique).toHaveBeenCalledTimes(1);
42+
expect(prisma.design_Review.update).toHaveBeenCalledTimes(1);
43+
expect(prismaDesignReview2.dateDeleted).toBeDefined();
44+
});
45+
46+
test('Delete Design Review succeeds when user is the creator of the design review', async () => {
47+
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview2);
48+
vi.spyOn(prisma.design_Review, 'update').mockResolvedValue(prismaDesignReview2);
49+
50+
expect(prismaDesignReview2.dateDeleted).toBeNull();
51+
52+
await DesignReviewService.deleteDesignReview(wonderwoman, prismaDesignReview2.designReviewId);
53+
54+
expect(prisma.design_Review.findUnique).toHaveBeenCalledTimes(1);
55+
expect(prisma.design_Review.update).toHaveBeenCalledTimes(1);
56+
expect(prismaDesignReview2.dateDeleted).toBeDefined();
57+
});
58+
});
59+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
Design_Review as PrismaDesignReview,
3+
Design_Review_Status as PrismaDesignReviewStatus,
4+
Prisma,
5+
TeamType
6+
} from '@prisma/client';
7+
import { batman, wonderwoman } from './users.test-data';
8+
import designReviewQueryArgs from '../../src/prisma-query-args/design-review.query-args';
9+
import { prismaWbsElement1 } from './wbs-element.test-data';
10+
11+
export const teamType1: TeamType = {
12+
teamTypeId: '1',
13+
name: 'teamType1'
14+
};
15+
16+
export const designReview1: PrismaDesignReview = {
17+
designReviewId: '1',
18+
dateScheduled: new Date('2024-03-25'),
19+
meetingTimes: [0, 1, 2, 3],
20+
dateCreated: new Date('2024-03-10'),
21+
userCreatedId: batman.userId,
22+
status: PrismaDesignReviewStatus.CONFIRMED,
23+
teamTypeId: '1',
24+
location: null,
25+
isOnline: true,
26+
isInPerson: false,
27+
zoomLink: null,
28+
dateDeleted: null,
29+
userDeletedId: null,
30+
docTemplateLink: null,
31+
wbsElementId: 1
32+
};
33+
34+
export const prismaDesignReview2: Prisma.Design_ReviewGetPayload<typeof designReviewQueryArgs> = {
35+
designReviewId: '2',
36+
dateScheduled: new Date('2024-03-25'),
37+
meetingTimes: [0, 4],
38+
dateCreated: new Date('2024-03-10'),
39+
userCreatedId: wonderwoman.userId,
40+
status: PrismaDesignReviewStatus.CONFIRMED,
41+
teamTypeId: '1',
42+
location: null,
43+
isOnline: true,
44+
isInPerson: false,
45+
zoomLink: null,
46+
dateDeleted: null,
47+
userDeletedId: null,
48+
docTemplateLink: null,
49+
wbsElementId: 1,
50+
userCreated: wonderwoman,
51+
requiredMembers: [wonderwoman],
52+
optionalMembers: [],
53+
confirmedMembers: [wonderwoman],
54+
deniedMembers: [],
55+
attendees: [wonderwoman],
56+
userDeleted: null,
57+
wbsElement: prismaWbsElement1,
58+
teamType: teamType1
59+
};

src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './src/types/work-package-types';
1010
export * from './src/types/team-types';
1111
export * from './src/types/task-types';
1212
export * from './src/types/reimbursement-requests-types';
13+
export * from './src/types/design-review-types';
1314

1415
export * from './src/validate-wbs';
1516
export * from './src/date-utils';

0 commit comments

Comments
 (0)