From b1faba71c6615f723644583fc963c7a6cb02380f Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Wed, 12 Nov 2025 02:08:41 -0800 Subject: [PATCH 1/6] added volunteer filter by Chapter, Role, Status --- .../graphql/resolvers/volunteers.resolvers.ts | 94 ++++++++++++++----- src/api/graphql/schemas/volunteers.schema.ts | 18 +++- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/api/graphql/resolvers/volunteers.resolvers.ts b/src/api/graphql/resolvers/volunteers.resolvers.ts index 25fffa6..cad69df 100644 --- a/src/api/graphql/resolvers/volunteers.resolvers.ts +++ b/src/api/graphql/resolvers/volunteers.resolvers.ts @@ -1,50 +1,100 @@ +import { GraphQLError } from 'graphql'; +import { z } from 'zod'; import { createVolunteer, createVolunteerSchema, deleteVolunteer, - getAllVolunteers, getVolunteerById, updateVolunteer, updateVolunteerSchema, } from '../../../core'; -import { GraphQLError } from 'graphql'; -import { z } from 'zod'; +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); -// Infer TypeScript types directly from your Zod schemas type CreateVolunteerInput = z.infer; type UpdateVolunteerInput = z.infer; +type VolunteerFilterInput = { + chapters?: string[]; // chapter names + roles?: string[]; // role + statuses?: string[]; // volunteer statuses +}; + export const volunteerResolvers = { Query: { - volunteers: () => getAllVolunteers(), + volunteers: async ( + _parent: unknown, + args: { + filter?: VolunteerFilterInput; + limit?: number; + offset?: number; + } + ) => { + const { filter, limit = 50, offset = 0 } = args; + + const where: any = {}; + + // status filter + if (filter?.statuses?.length) { + where.volunteer_status = { in: filter.statuses }; + } + + // chapter filter via chapters + if (filter?.chapters?.length) { + where.chapters = { name: { in: filter.chapters } }; + } + + // role filter via volunteers + if (filter?.roles?.length) { + where.volunteer_assignment = { + some: { + roles: { + name: { in: filter.roles }, + }, + }, + }; + } + + const [nodes, totalCount] = await Promise.all([ + prisma.volunteers.findMany({ + where, + skip: offset, + take: limit, + orderBy: [{ last_name: 'asc' }, { first_name: 'asc' }], + include: { + chapters: true, + companies: true, + volunteer_assignment: { + include: { + roles: true, + national_branches: true, + }, + }, + }, + }), + prisma.volunteers.count({ where }), + ]); + + return { nodes, totalCount }; + }, + volunteer: async (_parent: unknown, { id }: { id: string }) => { const volunteer = await getVolunteerById(id); if (!volunteer) { - throw new GraphQLError( - 'Volunteer not found', - undefined, // nodes - undefined, // source - undefined, // positions - undefined, // path - undefined, // originalError - { code: 'NOT_FOUND' } // extensions - ); + throw new GraphQLError('Volunteer not found', undefined, undefined, undefined, undefined, undefined, { + code: 'NOT_FOUND', + }); } return volunteer; }, }, + Mutation: { - createVolunteer: ( - _parent: unknown, - { input }: { input: CreateVolunteerInput } - ) => { + createVolunteer: (_parent: unknown, { input }: { input: CreateVolunteerInput }) => { const validatedInput = createVolunteerSchema.parse(input); return createVolunteer(validatedInput); }, - updateVolunteer: ( - _parent: unknown, - { id, input }: { id: string; input: UpdateVolunteerInput } - ) => { + updateVolunteer: (_parent: unknown, { id, input }: { id: string; input: UpdateVolunteerInput }) => { const validatedInput = updateVolunteerSchema.parse(input); return updateVolunteer(id, validatedInput); }, diff --git a/src/api/graphql/schemas/volunteers.schema.ts b/src/api/graphql/schemas/volunteers.schema.ts index d9c6831..1668f45 100644 --- a/src/api/graphql/schemas/volunteers.schema.ts +++ b/src/api/graphql/schemas/volunteers.schema.ts @@ -11,6 +11,12 @@ export const volunteerSchemaString = ` BOTH } + input VolunteerFilterInput { + chapters: [String!] + roles: [String!] + statuses: [String!] + } + type Volunteer { volunteer_id: ID! first_name: String! @@ -27,8 +33,18 @@ export const volunteerSchemaString = ` updated_at: String! } + type VolunteerConnection { + nodes: [Volunteer!]! + totalCount: Int! + } + type Query { - volunteers: [Volunteer!]! + volunteers( + filter: VolunteerFilterInput + limit: Int = 50 + offset: Int = 0 + ): VolunteerConnection! + volunteer(id: ID!): Volunteer } From b4ea205ed989778a293f7dc5d73696d31b102da5 Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Wed, 12 Nov 2025 02:11:22 -0800 Subject: [PATCH 2/6] ran prettier formatting --- .../graphql/resolvers/volunteers.resolvers.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/api/graphql/resolvers/volunteers.resolvers.ts b/src/api/graphql/resolvers/volunteers.resolvers.ts index cad69df..e973e16 100644 --- a/src/api/graphql/resolvers/volunteers.resolvers.ts +++ b/src/api/graphql/resolvers/volunteers.resolvers.ts @@ -16,7 +16,7 @@ type UpdateVolunteerInput = z.infer; type VolunteerFilterInput = { chapters?: string[]; // chapter names - roles?: string[]; // role + roles?: string[]; // role statuses?: string[]; // volunteer statuses }; @@ -81,20 +81,34 @@ export const volunteerResolvers = { volunteer: async (_parent: unknown, { id }: { id: string }) => { const volunteer = await getVolunteerById(id); if (!volunteer) { - throw new GraphQLError('Volunteer not found', undefined, undefined, undefined, undefined, undefined, { - code: 'NOT_FOUND', - }); + throw new GraphQLError( + 'Volunteer not found', + undefined, + undefined, + undefined, + undefined, + undefined, + { + code: 'NOT_FOUND', + } + ); } return volunteer; }, }, Mutation: { - createVolunteer: (_parent: unknown, { input }: { input: CreateVolunteerInput }) => { + createVolunteer: ( + _parent: unknown, + { input }: { input: CreateVolunteerInput } + ) => { const validatedInput = createVolunteerSchema.parse(input); return createVolunteer(validatedInput); }, - updateVolunteer: (_parent: unknown, { id, input }: { id: string; input: UpdateVolunteerInput }) => { + updateVolunteer: ( + _parent: unknown, + { id, input }: { id: string; input: UpdateVolunteerInput } + ) => { const validatedInput = updateVolunteerSchema.parse(input); return updateVolunteer(id, validatedInput); }, From e46b60c33998b75c8cb9d00374715a701742155a Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Wed, 12 Nov 2025 02:19:19 -0800 Subject: [PATCH 3/6] fixed linting --- .../graphql/resolvers/volunteers.resolvers.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/api/graphql/resolvers/volunteers.resolvers.ts b/src/api/graphql/resolvers/volunteers.resolvers.ts index e973e16..f8f2701 100644 --- a/src/api/graphql/resolvers/volunteers.resolvers.ts +++ b/src/api/graphql/resolvers/volunteers.resolvers.ts @@ -8,17 +8,21 @@ import { updateVolunteer, updateVolunteerSchema, } from '../../../core'; -import { PrismaClient } from '@prisma/client'; +import { + PrismaClient, + Prisma, + volunteer_status as VStatus, +} from '@prisma/client'; const prisma = new PrismaClient(); type CreateVolunteerInput = z.infer; type UpdateVolunteerInput = z.infer; -type VolunteerFilterInput = { - chapters?: string[]; // chapter names - roles?: string[]; // role - statuses?: string[]; // volunteer statuses -}; +export interface VolunteerFilterInput { + chapters?: string[]; + roles?: string[]; + statuses?: (import('@prisma/client').volunteer_status | string)[]; +} export const volunteerResolvers = { Query: { @@ -32,16 +36,26 @@ export const volunteerResolvers = { ) => { const { filter, limit = 50, offset = 0 } = args; - const where: any = {}; + const where: Prisma.volunteersWhereInput = {}; // status filter if (filter?.statuses?.length) { - where.volunteer_status = { in: filter.statuses }; + const statuses = filter.statuses + .map((s) => (typeof s === 'string' ? s.toUpperCase() : s)) + .filter((s): s is VStatus => s === 'STUDENT' || s === 'PROFESSIONAL'); + + if (statuses.length) { + where.volunteer_status = { in: statuses }; + } } // chapter filter via chapters if (filter?.chapters?.length) { - where.chapters = { name: { in: filter.chapters } }; + where.chapters = { + is: { + name: { in: filter.chapters }, + }, + }; } // role filter via volunteers From bfbb3b47cc085501aca77a74a81342b2f5f2532f Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Wed, 12 Nov 2025 02:25:55 -0800 Subject: [PATCH 4/6] removed comment --- src/api/graphql/schemas/volunteers.schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/graphql/schemas/volunteers.schema.ts b/src/api/graphql/schemas/volunteers.schema.ts index 1668f45..c943f46 100644 --- a/src/api/graphql/schemas/volunteers.schema.ts +++ b/src/api/graphql/schemas/volunteers.schema.ts @@ -1,5 +1,4 @@ export const volunteerSchemaString = ` - # Using the enum from your Prisma schema enum StatusType { STUDENT PROFESSIONAL From b565f31efd5f5723198f507a7d73bac5faef8c4a Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Wed, 12 Nov 2025 15:29:39 -0800 Subject: [PATCH 5/6] chore: enhance test setup by adding cleanup for volunteer-related models --- tests/setup.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/setup.ts b/tests/setup.ts index b43f316..954614f 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -29,11 +29,18 @@ beforeEach(async () => { // Skip cleanup for unit tests since they mock the database if (!isUnitTest) { // Delete all records in dependency order (children first, then parents) - await prisma.chapters.deleteMany({}); + + await prisma.volunteer_assignment.deleteMany({}); + await prisma.volunteer_role_project.deleteMany({}); + await prisma.volunteer_history.deleteMany({}); + + await prisma.volunteers.deleteMany({}); + + await prisma.nonprofit_chapter_project.deleteMany({}); + await prisma.sponsor_chapter.deleteMany({}); + await prisma.projects.deleteMany({}); - // Add other models as needed: - // await prisma.users.deleteMany({}); - // await prisma.events.deleteMany({}); + await prisma.chapters.deleteMany({}); } } catch (error) { console.error('Failed to clean database between tests:', error); From 445e9a9137826a3b283bf261bbf16ad3a5590ec8 Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Wed, 12 Nov 2025 18:09:10 -0800 Subject: [PATCH 6/6] feat: update volunteer GraphQL resolvers and schema for improved filtering and error handling --- .../graphql/resolvers/volunteers.resolvers.ts | 54 ++++++------------- src/api/graphql/schemas/volunteers.schema.ts | 2 +- tests/setup.ts | 4 -- 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/api/graphql/resolvers/volunteers.resolvers.ts b/src/api/graphql/resolvers/volunteers.resolvers.ts index f8f2701..ef33694 100644 --- a/src/api/graphql/resolvers/volunteers.resolvers.ts +++ b/src/api/graphql/resolvers/volunteers.resolvers.ts @@ -1,4 +1,5 @@ -import { GraphQLError } from 'graphql'; +// src/api/graphql/resolvers/volunteers.resolvers.ts + import { z } from 'zod'; import { createVolunteer, @@ -13,6 +14,7 @@ import { Prisma, volunteer_status as VStatus, } from '@prisma/client'; + const prisma = new PrismaClient(); type CreateVolunteerInput = z.infer; @@ -26,6 +28,8 @@ export interface VolunteerFilterInput { export const volunteerResolvers = { Query: { + // GraphQL: + // volunteers(filter, limit, offset): [Volunteer!]! volunteers: async ( _parent: unknown, args: { @@ -49,7 +53,7 @@ export const volunteerResolvers = { } } - // chapter filter via chapters + // chapter filter via related chapters model if (filter?.chapters?.length) { where.chapters = { is: { @@ -58,7 +62,7 @@ export const volunteerResolvers = { }; } - // role filter via volunteers + // role filter via volunteer_assignment → roles if (filter?.roles?.length) { where.volunteer_assignment = { some: { @@ -69,44 +73,20 @@ export const volunteerResolvers = { }; } - const [nodes, totalCount] = await Promise.all([ - prisma.volunteers.findMany({ - where, - skip: offset, - take: limit, - orderBy: [{ last_name: 'asc' }, { first_name: 'asc' }], - include: { - chapters: true, - companies: true, - volunteer_assignment: { - include: { - roles: true, - national_branches: true, - }, - }, - }, - }), - prisma.volunteers.count({ where }), - ]); - - return { nodes, totalCount }; + // Tests expect an array of volunteers here + return prisma.volunteers.findMany({ + where, + skip: offset, + take: limit, + orderBy: [{ last_name: 'asc' }, { first_name: 'asc' }], + }); }, + // GraphQL: volunteer(id: ID!): Volunteer volunteer: async (_parent: unknown, { id }: { id: string }) => { + // Let the service throw NotFoundError / DatabaseError as it already does. + // GraphQL will surface the error message in `errors[0].message`. const volunteer = await getVolunteerById(id); - if (!volunteer) { - throw new GraphQLError( - 'Volunteer not found', - undefined, - undefined, - undefined, - undefined, - undefined, - { - code: 'NOT_FOUND', - } - ); - } return volunteer; }, }, diff --git a/src/api/graphql/schemas/volunteers.schema.ts b/src/api/graphql/schemas/volunteers.schema.ts index c943f46..0ba1532 100644 --- a/src/api/graphql/schemas/volunteers.schema.ts +++ b/src/api/graphql/schemas/volunteers.schema.ts @@ -42,7 +42,7 @@ export const volunteerSchemaString = ` filter: VolunteerFilterInput limit: Int = 50 offset: Int = 0 - ): VolunteerConnection! + ): [Volunteer!]! volunteer(id: ID!): Volunteer } diff --git a/tests/setup.ts b/tests/setup.ts index 954614f..8035eff 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -28,14 +28,10 @@ beforeEach(async () => { // Skip cleanup for unit tests since they mock the database if (!isUnitTest) { - // Delete all records in dependency order (children first, then parents) - await prisma.volunteer_assignment.deleteMany({}); await prisma.volunteer_role_project.deleteMany({}); await prisma.volunteer_history.deleteMany({}); - await prisma.volunteers.deleteMany({}); - await prisma.nonprofit_chapter_project.deleteMany({}); await prisma.sponsor_chapter.deleteMany({});