From e9ae82f51f43186340b0fe8cd9b0be505c82abf2 Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Sun, 14 Dec 2025 13:47:42 -0800 Subject: [PATCH 1/8] feat: add chapter name filtering to nonprofits query --- .../graphql/resolvers/nonprofits.resolvers.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/api/graphql/resolvers/nonprofits.resolvers.ts b/src/api/graphql/resolvers/nonprofits.resolvers.ts index f38ed8a..0048236 100644 --- a/src/api/graphql/resolvers/nonprofits.resolvers.ts +++ b/src/api/graphql/resolvers/nonprofits.resolvers.ts @@ -6,6 +6,7 @@ import { getNonprofitsWithFilters, updateNonprofit, updateNonprofitSchema, + getChapterIdsByNames, } from '../../../core'; import { GraphQLError } from 'graphql'; import { z } from 'zod'; @@ -14,24 +15,42 @@ import type { NonprofitSortOption, } from '../../../core/services/nonprofits.service'; -// Infer TypeScript types directly from your Zod schemas type CreateNonprofitInput = z.infer; type UpdateNonprofitInput = z.infer; interface NonprofitsQueryArgs { chapterIds?: string[]; + chapterNames?: string[]; statuses?: StatusType[]; sort?: NonprofitSortOption[]; } export const nonprofitResolvers = { Query: { - nonprofits: ( + nonprofits: async ( _parent: unknown, - { chapterIds, statuses, sort }: NonprofitsQueryArgs + { chapterIds, chapterNames, statuses, sort }: NonprofitsQueryArgs ) => { - return getNonprofitsWithFilters({ chapterIds, statuses, sort }); + let resolvedChapterIds = chapterIds; + + // If frontend passes chapterNames, convert them to ids + if ( + (!resolvedChapterIds || resolvedChapterIds.length === 0) && + chapterNames?.length + ) { + resolvedChapterIds = await getChapterIdsByNames(chapterNames); + + // If user selected chapters but none matched, return empty list + if (!resolvedChapterIds || resolvedChapterIds.length === 0) return []; + } + + return getNonprofitsWithFilters({ + chapterIds: resolvedChapterIds, + statuses, + sort, + }); }, + nonprofit: async (_parent: unknown, { id }: { id: string }) => { const nonprofit = await getNonprofitById(id); if (!nonprofit) { @@ -48,6 +67,7 @@ export const nonprofitResolvers = { return nonprofit; }, }, + Mutation: { createNonprofit: ( _parent: unknown, @@ -56,6 +76,7 @@ export const nonprofitResolvers = { const validatedInput = createNonprofitSchema.parse(input); return createNonprofit(validatedInput); }, + updateNonprofit: ( _parent: unknown, { @@ -66,6 +87,7 @@ export const nonprofitResolvers = { const validatedInput = updateNonprofitSchema.parse(input); return updateNonprofit(nonprofit_id, validatedInput); }, + deleteNonprofit: (_parent: unknown, { id }: { id: string }) => { return deleteNonprofit(id); }, From 69c72c6a052b121d2bf2a446ca35a33f92f4f676 Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Sun, 14 Dec 2025 13:47:48 -0800 Subject: [PATCH 2/8] feat: add chapter names filtering to nonprofits query --- src/api/graphql/schemas/nonprofits.schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/graphql/schemas/nonprofits.schema.ts b/src/api/graphql/schemas/nonprofits.schema.ts index 46e71eb..7feda8d 100644 --- a/src/api/graphql/schemas/nonprofits.schema.ts +++ b/src/api/graphql/schemas/nonprofits.schema.ts @@ -21,6 +21,7 @@ export const nonprofitSchemaString = ` type Query { nonprofits( chapterIds: [ID!] + chapterNames: [String!] statuses: [StatusType!] sort: [NonprofitSortOption!] ): [Nonprofit!]! From 95c6306694187f02190999d684a21e9d7af8e4ed Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Sun, 14 Dec 2025 13:47:53 -0800 Subject: [PATCH 3/8] feat: add function to retrieve chapter IDs by names --- src/core/services/nonprofits.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/core/services/nonprofits.service.ts b/src/core/services/nonprofits.service.ts index 0b65630..e933788 100644 --- a/src/core/services/nonprofits.service.ts +++ b/src/core/services/nonprofits.service.ts @@ -519,3 +519,15 @@ export async function deleteNonprofit(id: string): Promise { throw new DatabaseError('Failed to delete nonprofit'); } } + +export async function getChapterIdsByNames(names: string[]): Promise { + const cleaned = [...new Set(names.map((n) => n.trim()).filter(Boolean))]; + if (cleaned.length === 0) return []; + + const chapters = await prisma.chapters.findMany({ + where: { name: { in: cleaned } }, + select: { chapter_id: true }, + }); + + return chapters.map((c) => c.chapter_id); +} From 6a2acdd608aa9896f6e5a84b6ffddee1d9e974c4 Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Thu, 18 Dec 2025 14:26:58 -0800 Subject: [PATCH 4/8] feat: add nonprofitChapterProjects query to retrieve projects by nonprofit and chapter IDs --- .../graphql/resolvers/nonprofits.resolvers.ts | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/api/graphql/resolvers/nonprofits.resolvers.ts b/src/api/graphql/resolvers/nonprofits.resolvers.ts index 0048236..ca384f9 100644 --- a/src/api/graphql/resolvers/nonprofits.resolvers.ts +++ b/src/api/graphql/resolvers/nonprofits.resolvers.ts @@ -14,6 +14,8 @@ import type { StatusType, NonprofitSortOption, } from '../../../core/services/nonprofits.service'; +import { prisma } from '../../../config/database'; +import { Prisma, status_type } from '@prisma/client'; type CreateNonprofitInput = z.infer; type UpdateNonprofitInput = z.infer; @@ -25,6 +27,12 @@ interface NonprofitsQueryArgs { sort?: NonprofitSortOption[]; } +interface NonprofitChapterProjectsArgs { + nonprofitIds?: string[]; + chapterIds?: string[]; + projectStatuses?: StatusType[]; +} + export const nonprofitResolvers = { Query: { nonprofits: async ( @@ -33,14 +41,11 @@ export const nonprofitResolvers = { ) => { let resolvedChapterIds = chapterIds; - // If frontend passes chapterNames, convert them to ids if ( (!resolvedChapterIds || resolvedChapterIds.length === 0) && chapterNames?.length ) { resolvedChapterIds = await getChapterIdsByNames(chapterNames); - - // If user selected chapters but none matched, return empty list if (!resolvedChapterIds || resolvedChapterIds.length === 0) return []; } @@ -66,6 +71,35 @@ export const nonprofitResolvers = { } return nonprofit; }, + + nonprofitChapterProjects: async ( + _parent: unknown, + { + nonprofitIds, + chapterIds, + projectStatuses, + }: NonprofitChapterProjectsArgs + ) => { + const where: Prisma.nonprofit_chapter_projectWhereInput = {}; + + if (nonprofitIds?.length) where.nonprofit_id = { in: nonprofitIds }; + if (chapterIds?.length) where.chapter_id = { in: chapterIds }; + + if (projectStatuses?.length) { + where.project_status = { + in: projectStatuses.map((s) => + s === 'ACTIVE' ? status_type.ACTIVE : status_type.INACTIVE + ), + }; + } + + const rows = await prisma.nonprofit_chapter_project.findMany({ + where, + orderBy: { created_at: 'desc' }, + }); + + return rows ?? []; + }, }, Mutation: { From cd1b889c7f80c709a3975585856d3fc31c916dda Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Thu, 18 Dec 2025 14:27:02 -0800 Subject: [PATCH 5/8] feat: add NonprofitChapterProject type and query for chapter projects --- src/api/graphql/schemas/nonprofits.schema.ts | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/api/graphql/schemas/nonprofits.schema.ts b/src/api/graphql/schemas/nonprofits.schema.ts index 7feda8d..ebc4b4f 100644 --- a/src/api/graphql/schemas/nonprofits.schema.ts +++ b/src/api/graphql/schemas/nonprofits.schema.ts @@ -18,6 +18,21 @@ export const nonprofitSchemaString = ` status: StatusType! } + type NonprofitChapterProject { + nonprofit_chapter_project_id: ID! + nonprofit_id: ID! + chapter_id: ID + project_id: ID! + project_contact_id: ID! + collab_contact_id: ID! + start_date: String! + end_date: String + notes: String + project_status: StatusType! + created_at: String! + updated_at: String! + } + type Query { nonprofits( chapterIds: [ID!] @@ -25,7 +40,14 @@ export const nonprofitSchemaString = ` statuses: [StatusType!] sort: [NonprofitSortOption!] ): [Nonprofit!]! + nonprofit(id: ID!): Nonprofit + + nonprofitChapterProjects( + nonprofitIds: [ID!] + chapterIds: [ID!] + projectStatuses: [StatusType!] + ): [NonprofitChapterProject!]! } input CreateNonprofitInput { From 43f65c61be3c02a7ad4971dad3f4e7b429cfa7be Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Thu, 18 Dec 2025 14:27:05 -0800 Subject: [PATCH 6/8] feat: implement Docker-based test database setup for isolated testing --- src/core/services/nonprofits.service.ts | 66 ++++++++++++++++--------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/core/services/nonprofits.service.ts b/src/core/services/nonprofits.service.ts index e933788..85130d0 100644 --- a/src/core/services/nonprofits.service.ts +++ b/src/core/services/nonprofits.service.ts @@ -7,7 +7,7 @@ import { DatabaseError, } from '../../middleware/error.middleware'; import { z } from 'zod'; -import { Prisma } from '@prisma/client'; +import { Prisma, status_type } from '@prisma/client'; type CreateNonprofitInput = z.infer; type UpdateNonprofitInput = z.infer; @@ -133,20 +133,49 @@ export async function getNonprofitsWithFilters( logger.info('Fetching nonprofits with filters', { filters }); - // Build Prisma where clause - const where: Prisma.nonprofitsWhereInput = {}; + const now = new Date(); + const activeProjectWhere: Prisma.nonprofit_chapter_projectWhereInput = { + project_status: status_type.ACTIVE, + OR: [{ end_date: null }, { end_date: { gt: now } }], + }; + + const andConditions: Prisma.nonprofitsWhereInput[] = []; + + // Chapter filter if (chapterIds && chapterIds.length > 0) { - where.nonprofit_chapter_project = { - some: { - chapter_id: { - in: chapterIds, + andConditions.push({ + nonprofit_chapter_project: { + some: { + chapter_id: { in: chapterIds }, }, }, - }; + }); } - // Fetch nonprofits with related projects + // Status filter - ACTIVE / INACTIVE + if (statuses && statuses.length > 0) { + const wantsActive = statuses.includes('ACTIVE'); + const wantsInactive = statuses.includes('INACTIVE'); + + if (wantsActive && !wantsInactive) { + andConditions.push({ + nonprofit_chapter_project: { + some: activeProjectWhere, + }, + }); + } else if (wantsInactive && !wantsActive) { + andConditions.push({ + nonprofit_chapter_project: { + none: activeProjectWhere, + }, + }); + } + } + + const where: Prisma.nonprofitsWhereInput = + andConditions.length > 0 ? { AND: andConditions } : {}; + const nonprofits = await prisma.nonprofits.findMany({ where, include: { @@ -159,10 +188,9 @@ export async function getNonprofitsWithFilters( }, }, }, - orderBy: { created_at: 'desc' }, // Default ordering + orderBy: { created_at: 'desc' }, }); - // Enrich nonprofits with derived fields const enrichedNonprofits: EnrichedNonprofit[] = nonprofits.map((np) => { const status = deriveNonprofitStatus(np.nonprofit_chapter_project); const latestStartDate = getLatestStartDate(np.nonprofit_chapter_project); @@ -181,25 +209,17 @@ export async function getNonprofitsWithFilters( }; }); - // Apply status filter - let filteredNonprofits = enrichedNonprofits; - if (statuses && statuses.length > 0) { - filteredNonprofits = enrichedNonprofits.filter((np) => - statuses.includes(np.status) - ); - } - - // Apply sorting + let result = enrichedNonprofits; if (sort && sort.length > 0) { const comparator = createNonprofitComparator(sort); - filteredNonprofits.sort(comparator); + result = [...enrichedNonprofits].sort(comparator); } logger.info('Successfully retrieved and filtered nonprofits', { - count: filteredNonprofits.length, + count: result.length, }); - return filteredNonprofits; + return result; } catch (error) { logger.error('Failed to fetch nonprofits with filters', { error: error instanceof Error ? error.message : 'Unknown error', From 23afe67ae9b2c80cc2c780ba17a255a418bd03bd Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Thu, 18 Dec 2025 14:27:21 -0800 Subject: [PATCH 7/8] ran prettier formatting --- README.md | 4 ++-- docker-compose.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index aeef139..0c383b9 100644 --- a/README.md +++ b/README.md @@ -230,8 +230,8 @@ Command to enter in terminal to access tables: - docker compose exec -T test-db psql -U postgres -d test_db - \dt -- SQL commands such as (SELECT * FROM volunteers;) +- SQL commands such as (SELECT \* FROM volunteers;) To pause test, insert this: await new Promise(() => {}); -It helps you see when data is created in the local database! \ No newline at end of file +It helps you see when data is created in the local database! diff --git a/docker-compose.yml b/docker-compose.yml index 54e7647..687474f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,12 +7,12 @@ services: POSTGRES_PASSWORD: test_password POSTGRES_DB: test_db ports: - - "5433:5432" # Map to port 5433 to avoid conflicts with local PostgreSQL + - '5433:5432' # Map to port 5433 to avoid conflicts with local PostgreSQL volumes: - test-db-data:/var/lib/postgresql/data - - ./db-init:/docker-entrypoint-initdb.d # Initialization scripts + - ./db-init:/docker-entrypoint-initdb.d # Initialization scripts healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d test_db"] + test: ['CMD-SHELL', 'pg_isready -U postgres -d test_db'] interval: 5s timeout: 5s retries: 5 From 7807039099fa4c240c4e59611d9963a2e1b79a14 Mon Sep 17 00:00:00 2001 From: amandayu255 Date: Thu, 18 Dec 2025 14:52:20 -0800 Subject: [PATCH 8/8] feat: enhance filtering logic for nonprofits with dynamic conditions --- src/core/services/nonprofits.service.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/services/nonprofits.service.ts b/src/core/services/nonprofits.service.ts index 85130d0..57aebaf 100644 --- a/src/core/services/nonprofits.service.ts +++ b/src/core/services/nonprofits.service.ts @@ -146,9 +146,7 @@ export async function getNonprofitsWithFilters( if (chapterIds && chapterIds.length > 0) { andConditions.push({ nonprofit_chapter_project: { - some: { - chapter_id: { in: chapterIds }, - }, + some: { chapter_id: { in: chapterIds } }, }, }); } @@ -173,8 +171,12 @@ export async function getNonprofitsWithFilters( } } - const where: Prisma.nonprofitsWhereInput = - andConditions.length > 0 ? { AND: andConditions } : {}; + let where: Prisma.nonprofitsWhereInput = {}; + if (andConditions.length === 1) { + where = andConditions[0]; + } else if (andConditions.length > 1) { + where = { AND: andConditions }; + } const nonprofits = await prisma.nonprofits.findMany({ where, @@ -210,9 +212,13 @@ export async function getNonprofitsWithFilters( }); let result = enrichedNonprofits; + if (statuses && statuses.length > 0) { + result = enrichedNonprofits.filter((np) => statuses.includes(np.status)); + } + if (sort && sort.length > 0) { const comparator = createNonprofitComparator(sort); - result = [...enrichedNonprofits].sort(comparator); + result = [...result].sort(comparator); } logger.info('Successfully retrieved and filtered nonprofits', {