Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d4d4d6e
#3622 basic FSAE parser
cielbellerose Sep 26, 2025
9019844
clean code + lettered bullets create new subrules
cielbellerose Oct 6, 2025
a31c186
#3622 FHE parser beginnings
cielbellerose Oct 15, 2025
79d5fc2
#3622 fhe little img info addition
cielbellerose Nov 3, 2025
d89bfb3
#3622 single parser class with FSAE/FHE inheritance
cielbellerose Nov 3, 2025
3c3bd88
#3622 small fsae formatting fix
cielbellerose Nov 3, 2025
f518513
fsae toc + duplicate temp fix + add to database
cielbellerose Nov 6, 2025
e64fbff
Merge branch 'feature/rules-dashboard' into 3622-rulesdashboard-parsing
cielbellerose Nov 19, 2025
d4d0448
#3622 comments added + small updates
cielbellerose Nov 19, 2025
9edeb0f
#3622 basic endpoint setup
cielbellerose Dec 3, 2025
c7c31ea
#3622 parse util functions progress
cielbellerose Dec 3, 2025
07eb8f1
#3622 simplified util functions
cielbellerose Dec 6, 2025
ce707ed
#3622 service update + added pdf parser dependency
cielbellerose Dec 6, 2025
aff6c9b
Merge branch 'feature/rules-dashboard' into 3622-rulesdashboard-parsing
cielbellerose Dec 6, 2025
0b07c8e
#3622 remove parser script version
cielbellerose Dec 6, 2025
6c6e283
#3622 frontend connections
cielbellerose Dec 6, 2025
190a3d3
#3622 fix page flow for file upload location
cielbellerose Dec 10, 2025
c468f07
Merge branch 'feature/rules-dashboard' into 3622-rulesdashboard-parsing
cielbellerose Dec 14, 2025
cc2155a
#3622 file modal updates
cielbellerose Dec 15, 2025
b578f54
#3622 connect car to file upload modal
cielbellerose Dec 15, 2025
612f3f2
Merge remote-tracking branch 'origin/feature/rules-dashboard' into 36…
cielbellerose Dec 24, 2025
9c85601
#3622 seed updates, route fixes, and temp console logs for file upload
cielbellerose Dec 24, 2025
c1a7dd8
#3622 upload file backend
cielbellerose Dec 24, 2025
d6d82a2
Merge remote-tracking branch 'origin/feature/rules-dashboard' into 36…
cielbellerose Dec 25, 2025
355a09f
#3622 ruleset pr fixes
cielbellerose Jan 2, 2026
935ea9b
Merge branch 'feature/rules-dashboard' into 3622-rulesdashboard-parsing
cielbellerose Jan 2, 2026
1defcd9
#3622 added ruleset pr items for ruleset table
cielbellerose Jan 2, 2026
f512d12
#3622 parser adds uploaded ruleset & RULES! view rules on edit page!
cielbellerose Jan 2, 2026
592a425
#3622 fix fsae header/footer & cleaning
cielbellerose Jan 3, 2026
853b148
Merge branch 'feature/rules-dashboard' into 3622-rulesdashboard-parsing
cielbellerose Jan 4, 2026
3119d5c
#3622 edit/view are disabled when parsing is happening
cielbellerose Jan 4, 2026
a3f7483
#3622 orphan fixes
cielbellerose Jan 4, 2026
9c3f328
Merge branch 'feature/rules-dashboard' into 3622-rulesdashboard-parsing
cielbellerose Jan 4, 2026
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
1 change: 1 addition & 0 deletions src/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.1",
"pdf-parse-new": "^1.4.1",
"prisma": "^6.2.1",
"shared": "1.0.0"
},
Expand Down
43 changes: 43 additions & 0 deletions src/backend/src/controllers/rules.controllers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextFunction, Request, Response } from 'express';
import RulesService from '../services/rules.services';
import { ProjectRule, Rule, Ruleset } from 'shared';
import { HttpException } from '../utils/errors.utils';

export default class RulesController {
static async getActiveRuleset(req: Request, res: Response, next: NextFunction) {
Expand All @@ -13,6 +14,16 @@ export default class RulesController {
}
}

static async getRulesetById(req: Request, res: Response, next: NextFunction) {
try {
const { rulesetId } = req.params;
const ruleset = await RulesService.getRulesetById(rulesetId, req.organization.organizationId);
res.status(200).json(ruleset);
} catch (error: unknown) {
next(error);
}
}

static async createRule(req: Request, res: Response, next: NextFunction) {
try {
const { ruleCode, ruleContent, rulesetId, parentRuleId, referencedRules, imageFileIds } = req.body;
Expand Down Expand Up @@ -273,4 +284,36 @@ export default class RulesController {
next(error);
}
}

static async parseRuleset(req: Request, res: Response, next: NextFunction) {
try {
const { fileId, parserType } = req.body;
const { rulesetId } = req.params;

const parseResult = await RulesService.parseRuleset(
req.currentUser,
req.organization.organizationId,
fileId,
rulesetId,
parserType
);

res.status(200).json(parseResult);
} catch (error: unknown) {
next(error);
}
}

static async uploadRulesetFile(req: Request, res: Response, next: NextFunction) {
try {
if (!req.file) {
throw new HttpException(400, 'Invalid or undefined file data');
}
const fileId = await RulesService.uploadRulesetFile(req.file, req.currentUser, req.organization);

res.status(200).json(fileId);
} catch (error: unknown) {
next(error);
}
}
}
15 changes: 15 additions & 0 deletions src/backend/src/prisma-query-args/rules.query-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,18 @@ export const getRulesetQueryArgs = () =>
}
}
});

export const getRulesetPreviewQueryArgs = () =>
Prisma.validator<Prisma.RulesetDefaultArgs>()({
select: {
name: true,
dateCreated: true,
rulesetType: true,
active: true,
car: {
include: {
wbsElement: true
}
}
}
});
10 changes: 9 additions & 1 deletion src/backend/src/prisma/seed-data/rules.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ const rulesetType1 = (userCreatedId: string, organizationId: string): Prisma.Rul
};
};

const rulesetType2 = (userCreatedId: string, organizationId: string): Prisma.Ruleset_TypeCreateInput => {
return {
name: 'FHE',
createdBy: { connect: { userId: userCreatedId } },
organization: { connect: { organizationId } }
};
};

const emptyRulesetType = (userCreatedId: string, organizationId: string): Prisma.Ruleset_TypeCreateInput => {
return {
name: 'Empty Ruleset Type',
Expand Down Expand Up @@ -115,7 +123,6 @@ const projectRule2 = (projectId: string, ruleId: string, createdByUserId: string

export const seedRulesetType = async (submitter: User, name: string, organization: Organization) => {
const createdRulesetType = await RulesService.createRulesetType(submitter, name, organization);

return createdRulesetType;
};

Expand All @@ -125,6 +132,7 @@ export const ruleSeedData = {
thirdLevelRule,
leafRule,
rulesetType1,
rulesetType2,
emptyRulesetType,
ruleset1,
secondActiveRuleset,
Expand Down
130 changes: 37 additions & 93 deletions src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import {
Task_Priority,
Task_Status,
Team,
Part_Tag,
Prisma,
Ruleset_Type
Part_Tag
} from '@prisma/client';
import { createUser, dbSeedAllUsers } from './seed-data/users.seed';
import { dbSeedAllTeams } from './seed-data/teams.seed';
Expand Down Expand Up @@ -51,7 +49,6 @@ import AnnouncementService from '../services/announcement.services';
import OnboardingServices from '../services/onboarding.services';
import { dbSeedAllParts, dbSeedAllPartTags } from './seed-data/parts.seed';
import FinanceServices from '../services/finance.services';
import { getUserQueryArgs } from '../prisma-query-args/user.query-args';
import { ruleSeedData } from './seed-data/rules.seed';
import RulesService from '../services/rules.services';
import { seedRulesetType } from './seed-data/rules.seed';
Expand Down Expand Up @@ -807,16 +804,6 @@ const performSeed: () => Promise<void> = async () => {
ner
);

/**
* Ruleset Types
*/

/** FSAE ruleset type */
const rulesetTypeFSAE = await seedRulesetType(joeShmoe, 'FSAE', ner);

/** FHE ruleset type */
const rulesetTypeFHE = await seedRulesetType(joeBlow, 'FHE', ner);

/**
* Graphs
*/
Expand Down Expand Up @@ -3073,43 +3060,6 @@ const performSeed: () => Promise<void> = async () => {
}
});

/**
* Rules
*/

// ruleset types
const fsaeRulesetType = await prisma.ruleset_Type.create({
data: ruleSeedData.rulesetType1(batman.userId, ner.organizationId)
});

const emptyRulesetType = await prisma.ruleset_Type.create({
data: ruleSeedData.emptyRulesetType(batman.userId, ner.organizationId)
});

// rulesets
const ruleset1 = await prisma.ruleset.create({
data: ruleSeedData.ruleset1(fergus.carId, batman.userId, fsaeRulesetType.rulesetTypeId)
});

const secondActiveRuleset = await prisma.ruleset.create({
data: ruleSeedData.secondActiveRuleset(fergus.carId, batman.userId, fsaeRulesetType.rulesetTypeId)
});

// rules
const ruleT = await prisma.rule.create({ data: ruleSeedData.topLevelRule(ruleset1.rulesetId, batman.userId) });
const ruleT2 = await prisma.rule.create({
data: ruleSeedData.secondLevelRule(ruleset1.rulesetId, batman.userId, ruleT.ruleId)
});
const ruleT21 = await prisma.rule.create({
data: ruleSeedData.thirdLevelRule(ruleset1.rulesetId, batman.userId, ruleT2.ruleId)
});
const ruleT211 = await prisma.rule.create({
data: ruleSeedData.leafRule(ruleset1.rulesetId, batman.userId, ruleT21.ruleId)
});

// project rules
await RulesService.createProjectRule(batman, ner, ruleT211.ruleId, project1Id);

const goldSponsorTier = await FinanceServices.createSponsorTier(thomasEmrax, 'Gold', ner, '#9F9156', 3000);
await FinanceServices.createSponsorTier(thomasEmrax, 'Silver', ner, '#C0C0C0', 200);
await FinanceServices.createSponsorTier(thomasEmrax, 'Bronze', ner, '#CD7F32', 10);
Expand Down Expand Up @@ -3140,18 +3090,31 @@ const performSeed: () => Promise<void> = async () => {
);

/**
* RULESET TYPES AND RULESETS
* Rules
*/

const formulaStudentRulesetType = await prisma.ruleset_Type.create({
data: {
name: 'Formula Student Rules',
createdByUserId: superman.userId,
organizationId: ner.organizationId
}
// ruleset types
const fsaeRulesetType = await prisma.ruleset_Type.create({
data: ruleSeedData.rulesetType1(batman.userId, ner.organizationId)
});

const fheRulesetType = await prisma.ruleset_Type.create({
data: ruleSeedData.rulesetType2(batman.userId, ner.organizationId)
});

const emptyRulesetType = await prisma.ruleset_Type.create({
data: ruleSeedData.emptyRulesetType(batman.userId, ner.organizationId)
});

// rulesets
const ruleset1 = await prisma.ruleset.create({
data: ruleSeedData.ruleset1(fergus.carId, batman.userId, fsaeRulesetType.rulesetTypeId)
});

const secondActiveRuleset = await prisma.ruleset.create({
data: ruleSeedData.secondActiveRuleset(fergus.carId, batman.userId, fsaeRulesetType.rulesetTypeId)
});

// Create rulesets
const fsae2025Ruleset = await prisma.ruleset.create({
data: {
fileId: 'fsae-2025-rules-file-id',
Expand All @@ -3174,9 +3137,21 @@ const performSeed: () => Promise<void> = async () => {
}
});

/**
* RULES
*/
// rules
const ruleT = await prisma.rule.create({ data: ruleSeedData.topLevelRule(ruleset1.rulesetId, batman.userId) });
const ruleT2 = await prisma.rule.create({
data: ruleSeedData.secondLevelRule(ruleset1.rulesetId, batman.userId, ruleT.ruleId)
});
const ruleT21 = await prisma.rule.create({
data: ruleSeedData.thirdLevelRule(ruleset1.rulesetId, batman.userId, ruleT2.ruleId)
});
const ruleT211 = await prisma.rule.create({
data: ruleSeedData.leafRule(ruleset1.rulesetId, batman.userId, ruleT21.ruleId)
});

// project rules
await RulesService.createProjectRule(batman, ner, ruleT211.ruleId, project1Id);

// Technical Rules Section
const techRule = await prisma.rule.create({
data: {
Expand Down Expand Up @@ -3227,13 +3202,6 @@ const performSeed: () => Promise<void> = async () => {
createdByUserId: thomasEmrax.userId
}
});
const rulesetType = await prisma.ruleset_Type.create({
data: {
name: 'FSAE',
createdByUserId: thomasEmrax.userId,
organizationId: ner.organizationId
}
});

// Powertrain Rules
const powertrainRule = await prisma.rule.create({
Expand Down Expand Up @@ -3512,30 +3480,6 @@ const performSeed: () => Promise<void> = async () => {
createdByUserId: thomasEmrax.userId
}
});

const ruleset = await prisma.ruleset.create({
data: {
name: 'FSAE Rules 2025',
fileId: 'fsae-rules-2025',
active: true,
dateCreated: new Date('2025-01-01T10:00:00Z'),
rulesetTypeId: rulesetType.rulesetTypeId,
createdByUserId: thomasEmrax.userId,
carId: fergus.carId
}
});

await prisma.rule.create({
data: {
ruleCode: 'T2.1.1',
ruleContent:
'The vehicle must be open-wheeled and open-cockpit (a formula style body) with four (4) wheels that are not in a straight line.',
imageFileIds: [],
dateCreated: new Date('2025-09-01T10:00:00Z'),
ruleset: { connect: { rulesetId: ruleset.rulesetId } },
createdBy: { connect: { userId: thomasEmrax.userId } }
}
});
};

performSeed()
Expand Down
14 changes: 14 additions & 0 deletions src/backend/src/routes/rules.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import express from 'express';
import RulesController from '../controllers/rules.controllers';
import { nonEmptyString, validateInputs } from '../utils/validation.utils';
import { body } from 'express-validator';
import { MAX_FILE_SIZE } from 'shared';
import multer, { memoryStorage } from 'multer';

const rulesRouter = express.Router();

rulesRouter.get('/rulesetType/:rulesetTypeId/active', RulesController.getActiveRuleset);
rulesRouter.get('/ruleset/:rulesetId', RulesController.getRulesetById);

rulesRouter.post(
'/rule/create',
Expand Down Expand Up @@ -89,4 +92,15 @@ rulesRouter.get('/ruleset/:rulesetId/project/:projectId/rules', RulesController.
rulesRouter.get('/:ruleId/subrules', RulesController.getChildRules);
rulesRouter.get('/:rulesetId/parentRules', RulesController.getTopLevelRules);

rulesRouter.post(
'/ruleset/:rulesetId/parse',
nonEmptyString(body('fileId')),
nonEmptyString(body('parserType')), // 'FSAE' or 'FHE'
validateInputs,
RulesController.parseRuleset
);

const upload = multer({ limits: { fileSize: MAX_FILE_SIZE }, storage: memoryStorage() });
rulesRouter.post('/upload/file', upload.single('file'), RulesController.uploadRulesetFile);

export default rulesRouter;
Loading