Skip to content
Open
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
5,193 changes: 3 additions & 5,190 deletions src/backend/src/prisma/dev-seed.ts

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions src/backend/src/prisma/factories/teams.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { Faker } from '@faker-js/faker';
import { Prisma, Team_Type } from '@prisma/client';
import type { FullUser } from '../context.js';

const SLACK_ID_RANDOM_LENGTH = 4;

export type SeedTeamConfig = {
teamName: string;
description: string;
teamTypeName: string;
financeTeam?: boolean;
};

export const seedTeamConfigs: SeedTeamConfig[] = [
{
teamName: 'Mechanical',
description: 'Designs and manufactures mechanical systems for the car.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Powertrain',
description: 'Develops drivetrain and high-voltage powertrain systems.',
teamTypeName: 'Electrical'
},
{
teamName: 'Electrical',
description: 'Builds and maintains the car electrical architecture.',
teamTypeName: 'Electrical'
},
{
teamName: 'Software',
description: 'Develops FinishLine, telemetry, and vehicle software.',
teamTypeName: 'Software'
},
{
teamName: 'Embedded Systems',
description: 'Works on firmware and embedded controls.',
teamTypeName: 'Software'
},
{
teamName: 'Controls',
description: 'Develops vehicle control systems and performance tools.',
teamTypeName: 'Software'
},
{
teamName: 'Battery',
description: 'Designs and validates the battery pack and accumulator systems.',
teamTypeName: 'Electrical'
},
{
teamName: 'Aerodynamics',
description: 'Designs aero components and validates vehicle airflow.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Chassis',
description: 'Develops the frame and structural systems.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Suspension',
description: 'Designs suspension geometry and handling systems.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Brakes',
description: 'Develops braking systems and pedal box components.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Composites',
description: 'Manufactures composite bodywork and structural parts.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Manufacturing',
description: 'Coordinates machining, fabrication, and shop work.',
teamTypeName: 'Mechanical'
},
{
teamName: 'Operations',
description: 'Coordinates logistics, planning, and internal processes.',
teamTypeName: 'Business'
},
{
teamName: 'Finance',
description: 'Manages purchasing, reimbursements, budgeting, and sponsor funds.',
teamTypeName: 'Business',
financeTeam: true
},
{
teamName: 'Sponsorship',
description: 'Manages sponsor outreach and sponsor relationships.',
teamTypeName: 'Business'
},
{
teamName: 'Marketing',
description: 'Creates team media, branding, and public-facing content.',
teamTypeName: 'Business'
},
{
teamName: 'Recruitment',
description: 'Supports recruiting, onboarding, and member engagement.',
teamTypeName: 'Business'
},
{
teamName: 'Data Acquisition',
description: 'Builds telemetry, sensors, and data analysis tools.',
teamTypeName: 'Electrical'
},
{
teamName: 'Driver Interface',
description: 'Develops cockpit, dashboard, and driver-facing controls.',
teamTypeName: 'Electrical'
}
];

const connectUsers = (users: FullUser[]) => users.map((user) => ({ userId: user.userId }));

const slackIdForTeam = (faker: Faker, teamName: string): string => {
const slug = teamName.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');

return `seed-${slug}-${faker.string.alphanumeric(SLACK_ID_RANDOM_LENGTH).toLowerCase()}`;
};

const findTeamType = (teamTypesByName: Record<string, Team_Type>, teamTypeName: string): Team_Type => {
const teamType = teamTypesByName[teamTypeName];

if (!teamType) {
throw new Error(`Missing team type: ${teamTypeName}`);
}

return teamType;
};

export const teamCreateInput = (
faker: Faker,
organizationId: string,
head: FullUser,
leads: FullUser[],
members: FullUser[],
teamTypesByName: Record<string, Team_Type>,
config: SeedTeamConfig,
overrides: Partial<Prisma.TeamCreateInput> = {}
): Prisma.TeamCreateInput => {
const teamType = findTeamType(teamTypesByName, config.teamTypeName);

return {
teamName: config.teamName,
slackId: slackIdForTeam(faker, config.teamName),
description: config.description,
financeTeam: config.financeTeam ?? false,
head: {
connect: { userId: head.userId }
},
leads: {
connect: connectUsers(leads)
},
members: {
connect: connectUsers(members)
},
teamType: {
connect: { teamTypeId: teamType.teamTypeId }
},
organization: {
connect: { organizationId }
},
...overrides
};
};
Empty file removed src/backend/src/prisma/index.ts
Empty file.
21 changes: 19 additions & 2 deletions src/backend/src/prisma/processes/seed-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,37 @@ export class SeedRunner {
if (!this.prisma) throw new Error('SeedRunner requires a PrismaClient. Call withPrisma() before run().');

const outputs = new Map<string, any>();
const context: Record<string, any> = {};

const mergeOutputs = (target: Record<string, any>, source: Record<string, any>, sourceName: string) => {
const duplicateKeys = Object.keys(source).filter((key) => key in target);

if (duplicateKeys.length > 0) {
throw new Error(`Duplicate seed output keys from ${sourceName}: ${duplicateKeys.join(', ')}`);
}

return Object.assign(target, source);
};

for (const instance of this.instances) {
instance.prisma = this.prisma;

const depOutputs = instance.dependencies().reduce((acc, depClass) => {
const depOutputs = instance.dependencies().reduce<Record<string, any>>((acc, depClass) => {
const output = outputs.get(depClass.name);
if (!output) throw new Error(`Missing output for dependency: ${depClass.name}`);
return { ...acc, ...output };

return mergeOutputs(acc, output, depClass.name);
}, {});

console.log(`Running ${instance.constructor.name} (seed ${GLOBAL_SEED})...`);
const output = await instance.run(depOutputs);

outputs.set(instance.constructor.name, output);
mergeOutputs(context, output, instance.constructor.name);

console.log(`${instance.constructor.name} complete`);
}

return context;
}
}
Empty file removed src/backend/src/prisma/random.ts
Empty file.
106 changes: 106 additions & 0 deletions src/backend/src/prisma/seed/team.process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Team, Team_Type } from '@prisma/client';
import { ConfigDataOutput, ConfigDataProcess } from './config-data.process.js';
import { OrganizationOutput, OrganizationProcess } from './organization.process.js';
import { UsersOutput, UsersProcess } from './user.process.js';
import { seedTeamConfigs, teamCreateInput } from '../factories/teams.factory.js';
import { SeedProcess } from '../processes/seed-process.js';
import type { FullUser } from '../context.js';

const MIN_LEADS_PER_TEAM = 1;
const MAX_LEADS_PER_TEAM = 3;
const MIN_MEMBERS_PER_TEAM = 8;
const MAX_MEMBERS_PER_TEAM = 20;

type TeamInput = OrganizationOutput & UsersOutput & ConfigDataOutput;

export type TeamOutput = {
teams: Team[];
financeTeam: Team;
teamsByName: Record<string, Team>;
};

export class TeamProcess extends SeedProcess<TeamInput, TeamOutput> {
dependencies() {
return [OrganizationProcess, UsersProcess, ConfigDataProcess];
}

async run({ organization, admins, heads, leadership, members, teamTypes }: TeamInput): Promise<TeamOutput> {
if (admins.length === 0 || heads.length === 0 || leadership.length === 0 || members.length === 0) {
throw new Error('TeamProcess requires admins, heads, leadership, and members to create teams.');
}

const teamNames = seedTeamConfigs.map((team) => team.teamName);

if (new Set(teamNames).size !== teamNames.length) {
throw new Error('TeamProcess cannot generate duplicate team names.');
}

const teamTypesByName = teamTypes.reduce<Record<string, Team_Type>>((acc, teamType) => {
acc[teamType.name] = teamType;
return acc;
}, {});

const leadershipCandidates = [...heads, ...admins, ...leadership];

if (leadershipCandidates.length < seedTeamConfigs.length) {
throw new Error(`Not enough head candidates (${leadershipCandidates.length}) for ${seedTeamConfigs.length} teams.`);
}

const usedLeadIds = new Set<string>();

const getLeadsForTeam = (head: FullUser): FullUser[] => {
const unusedLeadPool = leadershipCandidates.filter(
(user) => user.userId !== head.userId && !usedLeadIds.has(user.userId)
);

const fallbackLeadPool = leadershipCandidates.filter((user) => user.userId !== head.userId);
const leadPool = unusedLeadPool.length >= MIN_LEADS_PER_TEAM ? unusedLeadPool : fallbackLeadPool;

if (leadPool.length < MIN_LEADS_PER_TEAM) {
throw new Error('TeamProcess could not find enough leads for a team.');
}

const leads = this.faker.helpers.arrayElements(
leadPool,
this.faker.number.int({
min: MIN_LEADS_PER_TEAM,
max: Math.min(MAX_LEADS_PER_TEAM, leadPool.length)
})
);

leads.forEach((lead) => usedLeadIds.add(lead.userId));

return leads;
};

const teamCreateInputs = seedTeamConfigs.map((config, index) => {
const head = leadershipCandidates[index];
const leads = getLeadsForTeam(head);

const teamMembers = this.faker.helpers.arrayElements(
members,
this.faker.number.int({
min: MIN_MEMBERS_PER_TEAM,
max: MAX_MEMBERS_PER_TEAM
})
);

return teamCreateInput(this.faker, organization.organizationId, head, leads, teamMembers, teamTypesByName, config);
});

const teams = await Promise.all(teamCreateInputs.map((data) => this.prisma.team.create({ data })));

const teamsByName = teams.reduce<Record<string, Team>>((acc, team) => {
acc[team.teamName] = team;
return acc;
}, {});

const financeTeam = teamsByName.Finance;

if (!financeTeam) {
throw new Error('TeamProcess expected a Finance team to be generated.');
}

return { teams, financeTeam, teamsByName };
}
}
Loading