Skip to content
Draft
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
96 changes: 96 additions & 0 deletions apps/backend/src/lib/graphql/resolvers/awards/award-winner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { GraphQLFieldResolver } from 'graphql';
import db from '../../../database';
import { buildTeamGraphQL, TeamGraphQL } from '../../utils/team-builder';
import type { GraphQLContext } from '../../apollo-server';
import { requireAuthDivisionAndRole, requireAuthAndDivisionAccess } from '../../utils/auth-helpers';

// TODO: Replace with actual value from division/event settings
const AWARDS_ANNOUNCED = false;

// Allowed roles before awards are announced
const PRE_ANNOUNCEMENT_ROLES = ['judge-advisor'];

export interface AwardGraphQL {
id: string;
divisionId?: string;
name: string;
index: number;
place: number;
type: string;
isOptional: boolean;
allowNominations: boolean;
automaticAssignment: boolean;
description?: string;
}

export interface AwardWinnerGraphQL {
teamId: string;
divisionId: string; // Added to track division
}

/**
* Resolver for Award.winner field.
* Returns the computed winner for an award.
*
* Permission rules:
* - Before awards are announced: Requires judge-advisor role
* - After awards are announced: Accessible to anyone with division access
*
* @throws GraphQLError if user doesn't have appropriate permissions
*/
export const awardWinnerResolver: GraphQLFieldResolver<
AwardGraphQL,
GraphQLContext,
unknown,
Promise<AwardWinnerGraphQL | null>
> = async (award: AwardGraphQL, _args: unknown, context: GraphQLContext) => {
if (!award.divisionId) {
throw new Error('Award must have divisionId to resolve winner');
}

// If awards haven't been announced yet, require judge-advisor role
if (!AWARDS_ANNOUNCED) {
requireAuthDivisionAndRole(context.user, award.divisionId, PRE_ANNOUNCEMENT_ROLES);
} else {
// After announcement, only require basic division access
requireAuthAndDivisionAccess(context.user, award.divisionId);
}

try {
// TODO: Implement actual database query for award winners
// For now, return null as award winners aren't implemented yet
// When implemented, query should look like:
// const awardWinner = await db.awardWinners.byAwardId(award.id).get();

// Placeholder return
return null;
} catch (error) {
console.error('Error fetching award winner:', error);
throw error;
}
};

/**
* Resolver for AwardWinner.team field.
* Fetches the team that won the award.
*/
export const awardWinnerTeamResolver: GraphQLFieldResolver<
AwardWinnerGraphQL,
unknown,
unknown,
Promise<TeamGraphQL>
> = async (awardWinner: AwardWinnerGraphQL) => {
try {
const team = await db.teams.byId(awardWinner.teamId).get();

if (!team) {
throw new Error(`Team not found: ${awardWinner.teamId}`);
}

// Use the divisionId from the awardWinner object
return buildTeamGraphQL(team, awardWinner.divisionId);
} catch (error) {
console.error('Error fetching team for award winner:', error);
throw error;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ interface AwardsArgs {

export interface AwardGraphQL {
id: string;
divisionId: string; // Added for award winner resolver
name: string;
index: number;
place: number;
type: 'PERSONAL' | 'TEAM';
isOptional: boolean;
allowNominations: boolean;
automaticAssignment: boolean;
description?: string; // Added description field
}

/**
Expand All @@ -43,13 +45,15 @@ export const divisionAwardsResolver: GraphQLFieldResolver<

return awards.map(award => ({
id: award.id,
divisionId: division.id,
name: award.name,
index: award.index,
place: award.place,
type: award.type,
isOptional: award.is_optional,
allowNominations: award.allow_nominations,
automaticAssignment: award.automatic_assignment
automaticAssignment: award.automatic_assignment,
description: award.description
}));
} catch (error) {
console.error('Error fetching awards for division:', division.id, error);
Expand Down
46 changes: 46 additions & 0 deletions apps/backend/src/lib/graphql/resolvers/divisions/public-venue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { GraphQLFieldResolver } from 'graphql';
import db from '../../../database';

interface PublicDivisionVenueGraphQL {
id: string;
tables: Array<{ id: string; name: string }>;
rooms: Array<{ id: string; name: string }>;
}

/**
* Query resolver for fetching public venue information (tables and rooms) for a division.
* This resolver does not require authentication and is used during the login flow.
* @throws Error if division ID is not provided or division not found
*/
export const publicDivisionVenueResolver: GraphQLFieldResolver<
unknown,
unknown,
{ id: string },
Promise<PublicDivisionVenueGraphQL>
> = async (_parent, args) => {
if (!args.id) {
throw new Error('Division ID is required');
}

try {
// Verify division exists
const division = await db.divisions.byId(args.id).get();

if (!division) {
throw new Error(`Division with ID ${args.id} not found`);
}

// Fetch tables and rooms for this division
const tables = await db.tables.byDivisionId(args.id).getAll();
const rooms = await db.rooms.byDivisionId(args.id).getAll();

return {
id: args.id,
tables: tables.map(t => ({ id: t.id, name: t.name })),
rooms: rooms.map(r => ({ id: r.id, name: r.name }))
};
} catch (error) {
console.error('Error fetching public division venue:', error);
throw error;
}
};
13 changes: 12 additions & 1 deletion apps/backend/src/lib/graphql/resolvers/divisions/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import db from '../../../database';
import type { GraphQLContext } from '../../apollo-server';
import { requireAuthAndDivisionAccess } from '../../utils/auth-helpers';

/**
* Query resolver for fetching a single division by ID.
* Requires user to be authenticated and assigned to the division.
* @throws Error if division ID is not provided or division not found
* @throws GraphQLError if user is not authenticated or doesn't have access
*/
export const divisionResolver = async (_parent: unknown, args: { id: string }) => {
export const divisionResolver = async (
_parent: unknown,
args: { id: string },
context: GraphQLContext
) => {
if (!args.id) {
throw new Error('Division ID is required');
}

// Require authentication and division access
requireAuthAndDivisionAccess(context.user, args.id);

try {
const division = await db.divisions.byId(args.id).get();

Expand Down
13 changes: 11 additions & 2 deletions apps/backend/src/lib/graphql/resolvers/field/scoresheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import db from '../../../database';
import { buildTeamGraphQL, TeamGraphQL } from '../../utils/team-builder';
import { toGraphQLId } from '../../utils/object-id-transformer';
import { ScoresheetGraphQL } from '../../utils/scoresheet-builder';
import type { GraphQLContext } from '../../apollo-server';
import { requireAuthDivisionAndRole } from '../../utils/auth-helpers';

// Allowed roles for accessing scoresheet data
const SCORESHEET_ALLOWED_ROLES = ['referee', 'head-referee'];

/**
* Resolver for Scoresheet.team field.
Expand All @@ -24,13 +29,17 @@ export const scoresheetTeamResolver: GraphQLFieldResolver<
/**
* Resolver for Scoresheet.data field.
* Returns the scoresheet data if it exists.
* Requires user to be a referee or head-referee in the division.
*/
export const scoresheetDataResolver: GraphQLFieldResolver<
ScoresheetGraphQL,
unknown,
GraphQLContext,
unknown,
ScoresheetGraphQL['data'] | null
> = (scoresheet: ScoresheetGraphQL) => {
> = (scoresheet: ScoresheetGraphQL, _args: unknown, context: GraphQLContext) => {
// Check authentication, division access, and role permissions
requireAuthDivisionAndRole(context.user, scoresheet.divisionId, SCORESHEET_ALLOWED_ROLES);

return scoresheet.data || null;
};

Expand Down
11 changes: 10 additions & 1 deletion apps/backend/src/lib/graphql/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GraphQLScalarType, Kind } from 'graphql';
import { eventResolvers } from './events/resolver';
import { divisionResolver } from './divisions/resolver';
import { publicDivisionVenueResolver } from './divisions/public-venue';
import { isFullySetUpResolver } from './events/is-fully-set-up';
import { eventDivisionsResolver } from './events/event-divisions';
import {
Expand Down Expand Up @@ -47,6 +48,7 @@ import { fieldScoresheetsResolver } from './divisions/field/scoresheets';
import { RubricUpdatedEventResolver } from './subscriptions/rubrics/rubric-updated';
import { ScoresheetUpdatedEventResolver } from './subscriptions/scoresheet/scoresheet-updated';
import { DeliberationUpdatedEventResolver } from './subscriptions/deliberations/deliberation-updated';
import { awardWinnerResolver, awardWinnerTeamResolver } from './awards/award-winner';

// JSON scalar resolver - passes through any valid JSON value
const JSONScalar = new GraphQLScalarType({
Expand Down Expand Up @@ -81,7 +83,8 @@ export const resolvers = {
Query: {
events: eventResolvers.Query.events,
event: eventResolvers.Query.event,
division: divisionResolver
division: divisionResolver,
publicDivisionVenue: publicDivisionVenueResolver
},
Mutation: mutationResolvers,
Subscription: subscriptionResolvers,
Expand Down Expand Up @@ -142,6 +145,12 @@ export const resolvers = {
team: scoresheetTeamResolver,
data: scoresheetDataResolver
},
Award: {
winner: awardWinnerResolver
},
AwardWinner: {
team: awardWinnerTeamResolver
},
Volunteer: {
divisions: volunteerDivisionsResolver
},
Expand Down
13 changes: 11 additions & 2 deletions apps/backend/src/lib/graphql/resolvers/judging/rubric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import db from '../../../database';
import { toGraphQLId } from '../../utils/object-id-transformer';
import { buildTeamGraphQL, TeamGraphQL } from '../../utils/team-builder';
import { RubricGraphQL } from '../../utils/rubric-builder';
import type { GraphQLContext } from '../../apollo-server';
import { requireAuthDivisionAndRole } from '../../utils/auth-helpers';

// Allowed roles for accessing rubric data
const RUBRIC_ALLOWED_ROLES = ['judge', 'lead-judge', 'judge-advisor'];

/**
* Resolver for Rubric.team field.
Expand All @@ -25,13 +30,17 @@ export const rubricTeamResolver: GraphQLFieldResolver<
/**
* Resolver for Rubric.data field.
* Returns the rubric data if it exists.
* Requires user to be a judge, lead-judge, or judge-advisor in the division.
*/
export const rubricDataResolver: GraphQLFieldResolver<
RubricGraphQL,
unknown,
GraphQLContext,
unknown,
RubricGraphQL['data'] | null
> = (rubric: RubricGraphQL) => {
> = (rubric: RubricGraphQL, _args: unknown, context: GraphQLContext) => {
// Check authentication, division access, and role permissions
requireAuthDivisionAndRole(context.user, rubric.divisionId, RUBRIC_ALLOWED_ROLES);

return rubric.data || null;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RedisEventTypes } from '@lems/types/api/lems/redis';
import { getRedisPubSub } from '../../../../redis/redis-pubsub';
import type { GraphQLContext } from '../../../apollo-server';
import { requireAuthAndDivisionAccess } from '../../../utils/auth-helpers';

interface JudgingSessionStartedSubscribeArgs {
divisionId: string;
Expand Down Expand Up @@ -34,9 +36,11 @@ const processJudgingSessionStartedEvent = async (

const judgingSessionStartedSubscribe = (
_root: unknown,
{ divisionId }: JudgingSessionStartedSubscribeArgs
{ divisionId }: JudgingSessionStartedSubscribeArgs,
context: GraphQLContext
) => {
if (!divisionId) throw new Error('divisionId is required');
requireAuthAndDivisionAccess(context.user, divisionId);
const pubSub = getRedisPubSub();
return pubSub.asyncIterator(divisionId, RedisEventTypes.JUDGING_SESSION_STARTED);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { RedisEventTypes } from '@lems/types/api/lems/redis';
import { getRedisPubSub } from '../../../../redis/redis-pubsub';
import type { GraphQLContext } from '../../../apollo-server';
import { requireAuthDivisionAndRole } from '../../../utils/auth-helpers';

// Allowed roles for accessing rubric data
const RUBRIC_ALLOWED_ROLES = ['judge', 'lead-judge', 'judge-advisor'];

interface RubricStatusChangedSubscribeArgs {
divisionId: string;
Expand All @@ -21,8 +26,14 @@ async function processRubricStatusChangedEvent(
}

export const rubricStatusChangedResolver = {
subscribe: (_root: unknown, { divisionId }: RubricStatusChangedSubscribeArgs) => {
subscribe: (
_root: unknown,
{ divisionId }: RubricStatusChangedSubscribeArgs,
context: GraphQLContext
) => {
if (!divisionId) throw new Error('divisionId is required');
// Require authentication, division access, and appropriate role for rubric data
requireAuthDivisionAndRole(context.user, divisionId, RUBRIC_ALLOWED_ROLES);
const pubSub = getRedisPubSub();
return pubSub.asyncIterator(divisionId, RedisEventTypes.RUBRIC_STATUS_CHANGED);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { RedisEventTypes } from '@lems/types/api/lems/redis';
import { getRedisPubSub } from '../../../../redis/redis-pubsub';
import type { GraphQLContext } from '../../../apollo-server';
import { requireAuthDivisionAndRole } from '../../../utils/auth-helpers';

// Allowed roles for accessing rubric data
const RUBRIC_ALLOWED_ROLES = ['judge', 'lead-judge', 'judge-advisor'];

interface RubricUpdatedSubscribeArgs {
divisionId: string;
Expand Down Expand Up @@ -110,8 +115,14 @@ async function processRubricUpdatedEvent(
}

export const rubricUpdatedResolver = {
subscribe: (_root: unknown, { divisionId }: RubricUpdatedSubscribeArgs) => {
subscribe: (
_root: unknown,
{ divisionId }: RubricUpdatedSubscribeArgs,
context: GraphQLContext
) => {
if (!divisionId) throw new Error('divisionId is required');
// Require authentication, division access, and appropriate role for rubric data
requireAuthDivisionAndRole(context.user, divisionId, RUBRIC_ALLOWED_ROLES);
const pubSub = getRedisPubSub();
return pubSub.asyncIterator(divisionId, RedisEventTypes.RUBRIC_UPDATED);
},
Expand Down
Loading