Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/backend/src/controllers/rules.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default class RulesController {

static async getRulesetsByRulesetType(req: Request, res: Response, next: NextFunction) {
try {
const rulesetTypeId = req.body;
const { rulesetTypeId } = req.params;
const rulesets = await RulesService.getRulesetsByRulesetType(rulesetTypeId, req.organization.organizationId);
res.status(200).json(rulesets);
} catch (error: unknown) {
Expand Down
17 changes: 2 additions & 15 deletions src/backend/src/prisma-query-args/rules.query-args.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Prisma } from '@prisma/client';
import { getUserQueryArgs } from './user.query-args';

export type RulePreviewQueryArgs = ReturnType<typeof getRulePreviewQueryArgs>;

Expand Down Expand Up @@ -50,7 +49,7 @@ export const getProjectRuleQueryArgs = () =>

export type RulesetQueryArgs = ReturnType<typeof getRulesetQueryArgs>;

export const getRulesetQueryArgs = (organizationId: string) =>
export const getRulesetQueryArgs = () =>
Prisma.validator<Prisma.RulesetDefaultArgs>()({
include: {
rules: {
Expand All @@ -65,20 +64,8 @@ export const getRulesetQueryArgs = (organizationId: string) =>
}
},
rulesetType: true,
car: true,
createdBy: getUserQueryArgs(organizationId)
}
});

export const getRulesetPreviewQueryArgs = () =>
Prisma.validator<Prisma.RulesetDefaultArgs>()({
select: {
name: true,
dateCreated: true,
rulesetType: true,
active: true,
car: {
select: {
include: {
wbsElement: true
}
}
Expand Down
2 changes: 0 additions & 2 deletions src/backend/src/routes/rules.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ rulesRouter.post('/rule/:ruleId/delete', RulesController.deleteRule);

rulesRouter.post('/rulesetType/create', nonEmptyString(body('name')), validateInputs, RulesController.createRulesetType);

rulesRouter.post('/rule/:ruleId/delete', RulesController.deleteRule);

rulesRouter.post(
'/projectRule/create',
nonEmptyString(body('ruleId')),
Expand Down
41 changes: 23 additions & 18 deletions src/backend/src/services/rules.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
ProjectRule,
RulesetType,
notGuest,
RulesetPreview,
User,
Rule as SharedRule,
isHead
isHead,
Ruleset
} from 'shared';
import prisma from '../prisma/prisma';
import {
Expand All @@ -24,15 +24,13 @@ import { userHasPermission } from '../utils/users.utils';
import {
getProjectRuleQueryArgs,
getRulesetQueryArgs,
getRulesetPreviewQueryArgs,
getRulePreviewQueryArgs
} from '../prisma-query-args/rules.query-args';
import {
ruleTransformer,
projectRuleTransformer,
rulesetTransformer,
rulesetTypeTransformer,
rulesetPreviewTransformer
rulesetTypeTransformer
} from '../transformers/rules.transformer';

export default class RulesService {
Expand Down Expand Up @@ -61,7 +59,7 @@ export default class RulesService {

const activeRuleset = await prisma.ruleset.findFirst({
where: { rulesetTypeId, deletedByUserId: null, active: true },
...getRulesetQueryArgs(organization.organizationId)
...getRulesetQueryArgs()
});

if (!activeRuleset) {
Expand Down Expand Up @@ -461,10 +459,10 @@ export default class RulesService {
* @param organizationId the id of the organization the ruleset is being deleted in
* @returns the ruleset with query args
*/
static async getRulesetWithQueryArgs(rulesetId: string, organizationId: string) {
static async getRulesetWithQueryArgs(rulesetId: string) {
const ruleset = await prisma.ruleset.findUnique({
where: { rulesetId },
...getRulesetQueryArgs(organizationId)
...getRulesetQueryArgs()
});

if (!ruleset) throw new NotFoundException('Ruleset', rulesetId);
Expand All @@ -481,17 +479,21 @@ export default class RulesService {
* @returns the deleted Ruleset
*/
static async deleteRuleset(rulesetId: string, deleterId: string, organizationId: string) {
const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId, organizationId);
const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId);

const hasPermission =
(await userHasPermission(deleterId, organizationId, isAdmin)) || deleterId === ruleset.createdByUserId;

if (!hasPermission) throw new AccessDeniedException('Only admins can delete a ruleset.');

if (ruleset.active) {
throw new HttpException(400, 'Cannot delete an active ruleset. Please deactivate it first.');
}

const deletedRuleset = await prisma.ruleset.update({
where: { rulesetId },
data: { deletedBy: { connect: { userId: deleterId } } },
...getRulesetQueryArgs(organizationId)
data: { deletedBy: { connect: { userId: deleterId } }, active: false },
...getRulesetQueryArgs()
});

return rulesetTransformer(deletedRuleset);
Expand All @@ -516,7 +518,7 @@ export default class RulesService {
* @param organizationId id of organization
* @returns rulesets associated with provided ruleset type
*/
static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string): Promise<RulesetPreview[]> {
static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string): Promise<Ruleset[]> {
const rulesets = await prisma.ruleset.findMany({
where: {
rulesetTypeId,
Expand All @@ -525,10 +527,13 @@ export default class RulesService {
organizationId
}
},
...getRulesetPreviewQueryArgs()
orderBy: {
dateCreated: 'desc'
},
...getRulesetQueryArgs()
});

return rulesets.map(rulesetPreviewTransformer);
return rulesets.map(rulesetTransformer);
}

/**
Expand Down Expand Up @@ -573,7 +578,7 @@ export default class RulesService {
active: true,
deletedBy: null
},
...getRulesetQueryArgs(organization.organizationId)
...getRulesetQueryArgs()
});

if (!activeRuleset) {
Expand Down Expand Up @@ -784,7 +789,7 @@ export default class RulesService {
active,
createdByUserId: submitter.userId
},
...getRulesetQueryArgs(organization.organizationId)
...getRulesetQueryArgs()
});

return rulesetTransformer(ruleset);
Expand Down Expand Up @@ -942,7 +947,7 @@ export default class RulesService {
rulesetTypeId: rulesetExists.rulesetTypeId,
organizationId
},
dateDeleted: null
deletedByUserId: null
}
});

Expand All @@ -961,7 +966,7 @@ export default class RulesService {
name,
active: isActive
},
...getRulesetQueryArgs(organizationId)
...getRulesetQueryArgs()
});

return rulesetTransformer(ruleset);
Expand Down
21 changes: 4 additions & 17 deletions src/backend/src/transformers/rules.transformer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Prisma } from '@prisma/client';
import { Rule, ProjectRule, Ruleset, RulesetType, RulesetPreview } from 'shared';
import { Rule, ProjectRule, Ruleset, RulesetType } from 'shared';
import { RulesetQueryArgs, RulePreviewQueryArgs } from '../prisma-query-args/rules.query-args';

export const ruleTransformer = (rule: Prisma.RuleGetPayload<RulePreviewQueryArgs>): Rule => {
Expand Down Expand Up @@ -35,6 +35,8 @@ export const rulesetTypeTransformer = (rulesetType: any): RulesetType => {
name: rulesetType.name,
lastUpdated: rulesetType.lastUpdated,
revisionFiles: rulesetType.revisionFiles
? rulesetType.revisionFiles.filter((ruleset: any) => ruleset.deletedByUserId === null)
: []
};
};

Expand All @@ -53,22 +55,7 @@ export const rulesetTransformer = (ruleset: Prisma.RulesetGetPayload<RulesetQuer
rulesetType: rulesetTypeTransformer(ruleset.rulesetType),
car: {
carId: ruleset.car.carId,
name: ruleset.car.wbsElementId
}
};
};

export const rulesetPreviewTransformer = (ruleset: any): RulesetPreview => {
const teamsPercentage = 0;

return {
name: ruleset.name,
dateCreated: ruleset.dateCreated,
active: ruleset.active,
assignedPercentage: teamsPercentage,
car: {
carId: ruleset.car.carId,
name: ruleset.car.wbsElementId
name: ruleset.car.wbsElement.name
}
};
};
48 changes: 46 additions & 2 deletions src/backend/tests/unit/rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,12 @@ describe('Create Rules Tests', () => {
});

it('Successful get rulesets by ruleset types after deleting ruleset', async () => {
// Deactivate the ruleset before deleting
await prisma.ruleset.update({
where: { rulesetId },
data: { active: false }
});

await RulesService.deleteRuleset(rulesetId, batman.userId, orgId);
const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId);
expect(rulesets.length).toBe(0);
Expand All @@ -395,8 +401,8 @@ describe('Create Rules Tests', () => {
});
const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId);
expect(rulesets.length).toBe(2);
expect(rulesets[0].name).toBe('2025 FSAE Rules');
expect(rulesets[1].name).toBe('2025 FSAE Rules2');
expect(rulesets[0].name).toBe('2025 FSAE Rules2');
expect(rulesets[1].name).toBe('2025 FSAE Rules');
});
});

Expand Down Expand Up @@ -524,6 +530,11 @@ describe('Create Rules Tests', () => {
expect(ruleset2.name).toBe('name2');
});
it('update ruleset status on deleted ruleset fails', async () => {
// Deactivate the ruleset before deleting
await prisma.ruleset.update({
where: { rulesetId },
data: { active: false }
});
await RulesService.deleteRuleset(rulesetId, batman.userId, orgId);
await expect(async () => await RulesService.updateRuleset(batman, orgId, rulesetId, 'name', false)).rejects.toThrow(
new NotFoundException('Ruleset', rulesetId)
Expand Down Expand Up @@ -973,6 +984,13 @@ describe('Rule Tests', () => {
it('Deletes a ruleset successfully and returns the correct information', async () => {
const car = await createUniqueCar(orgId);
const { ruleset1 } = await setupRules(car);

// deactivate before deleting
await prisma.ruleset.update({
where: { rulesetId: ruleset1.rulesetId },
data: { active: false }
});

const totalRules = await prisma.rule.count({
where: { rulesetId: ruleset1.rulesetId }
});
Expand All @@ -989,6 +1007,20 @@ describe('Rule Tests', () => {
expect(deleted.rulesetId).toBe(ruleset1.rulesetId);
expect(deleted.assignedPercentage).toBeCloseTo(expectedPercentage, 2);
});
it('Throws error when trying to delete an active ruleset', async () => {
const car = await createUniqueCar(orgId);
const { ruleset1 } = await setupRules(car);

// Ensure the ruleset is active
await prisma.ruleset.update({
where: { rulesetId: ruleset1.rulesetId },
data: { active: true }
});

await expect(
RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId)
).rejects.toThrow('Cannot delete an active ruleset. Please deactivate it first.');
});
it('Delete ruleset fails if user does not have permission', async () => {
const car = await createUniqueCar(orgId);
const { ruleset1 } = await setupRules(car);
Expand All @@ -1001,6 +1033,12 @@ describe('Rule Tests', () => {
const car = await createUniqueCar(orgId);
const { ruleset1 } = await setupRules(car);

// Deactivate the ruleset before deleting
await prisma.ruleset.update({
where: { rulesetId: ruleset1.rulesetId },
data: { active: false }
});

await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId);
await expect(
async () => await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId)
Expand Down Expand Up @@ -1455,6 +1493,12 @@ describe('Rule Tests', () => {
const car = await createUniqueCar(orgId);
const { ruleset1 } = await setupRules(car);

// Deactivate the ruleset before deleting
await prisma.ruleset.update({
where: { rulesetId: ruleset1.rulesetId },
data: { active: false }
});

await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId);

await expect(RulesService.getUnassignedRules(ruleset1.rulesetId, organization)).rejects.toThrow(
Expand Down
21 changes: 21 additions & 0 deletions src/frontend/src/apis/rules.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,24 @@ export const getRulesetsByRulesetType = (rulesetTypeId: string) => {
transformResponse: (data) => JSON.parse(data)
});
};

/**
* Updates a rulesets active status
*/
export const updateRuleset = (rulesetId: string, name: string, isActive: boolean) => {
return axios.post<Ruleset>(apiUrls.rulesetUpdate(rulesetId), { name, isActive });
};

/**
* Deletes a ruleset given its ID
*/
export const deleteRuleset = (rulesetId: string) => {
return axios.post(apiUrls.rulesetDelete(rulesetId));
};

/**
* Deletes a ruleset type given its ID
*/
export const deleteRulesetType = (rulesetTypeId: string) => {
return axios.post(apiUrls.rulesetTypeDelete(rulesetTypeId));
};
Loading