Skip to content

Commit 071f816

Browse files
Add nonprofit sorting functionality (#72)
Closes #62 ### Overview This PR implements sorting and filtering capabilities for the nonprofits GraphQL endpoint. The goal was to enable users to sort nonprofits alphabetically (A-Z and Z-A), by most recent project start date, and by status (ACTIVE/INACTIVE), as well as filter nonprofits by chapter IDs and status. **Key Changes:** - Added `NonprofitSortOption` enum with four sorting options: `A_TO_Z`, `Z_TO_A`, `MOST_RECENT`, and `STATUS` - Implemented derived status computation logic that determines if a nonprofit is ACTIVE or INACTIVE based on its associated projects - Added `status` field to the `Nonprofit` GraphQL type - Enhanced the `nonprofits` query to accept optional `chapterIds`, `statuses`, and `sort` parameters - Implemented multi-level sorting that allows combining multiple sort options in order of precedence - Added comprehensive filtering by chapter IDs and status types **Implementation Details:** - Status is derived dynamically from `nonprofit_chapter_project` relationships (ACTIVE if there's an ongoing project, INACTIVE otherwise) - Sorting is performed in-memory after fetching nonprofits to support complex multi-level sorting - All query parameters are optional for backward compatibility - Chapter filtering uses Prisma relation filters for efficiency ### Testing **Unit Tests:** - Added comprehensive unit tests in `tests/unit/services/nonprofits.service.test.ts` covering: - Status derivation logic (ACTIVE vs INACTIVE based on project dates and status) - A-Z and Z-A alphabetical sorting - Most recent sorting (by latest project start date) - Status sorting (ACTIVE before INACTIVE) - Combined multi-level sorting (e.g., STATUS + MOST_RECENT + A_TO_Z) - Chapter filtering - Status filtering - Edge cases (nonprofits without projects, multiple projects, etc.) **Integration Tests:** - Added new integration test file `tests/integration/graphql/nonprofits-sorting.test.ts` with end-to-end GraphQL tests: - A to Z sorting - Z to A sorting - Most recent sorting - Status sorting - Status filtering (ACTIVE only) - Chapter filtering - Combined sorting (STATUS + MOST_RECENT) All tests pass successfully. The implementation maintains backward compatibility - existing queries without the new parameters continue to work as before. ### Screenshots / Screencasts N/A - This is a backend API change with no frontend components affected. ### Checklist - [x] Code is neat, readable, and works - [x] Code is commented where appropriate and well-documented - [x] Commit messages follow our [guidelines](https://www.conventionalcommits.com) - [x] Issue number is linked - [x] Branch is linked - [x] Reviewers are assigned (one of your tech leads) ### Notes - The status field is computed dynamically on each query rather than being persisted in the database, which ensures accuracy but may have performance implications for very large datasets - Nonprofits without any projects are considered INACTIVE - When sorting by MOST_RECENT, nonprofits without projects appear last - The implementation follows the existing codebase patterns and maintains consistency with other similar features (e.g., project sorting) - All linting errors have been resolved and the code passes ESLint checks --------- Co-authored-by: Sophia Chang <[email protected]>
1 parent 4c35188 commit 071f816

File tree

13 files changed

+1336
-73
lines changed

13 files changed

+1336
-73
lines changed

.DS_Store

6 KB
Binary file not shown.

.env.example

Lines changed: 0 additions & 23 deletions
This file was deleted.

.env.test

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
# Server Configuration (using different ports for testing)
2-
PORT=4001
3-
REST_PORT=4002
4-
GRAPHQL_PORT=4003
1+
# Test Database Configuration
2+
DATABASE_URL=postgresql://postgres:test_password@localhost:5433/test_db?schema=public
3+
DIRECT_URL=postgresql://postgres:test_password@localhost:5433/test_db?schema=public
54

6-
# Application Configuration
5+
# Environment
76
NODE_ENV=test
8-
SEED_DATA=false
97

10-
# Optional: Add any API keys or other configuration
11-
# JWT_SECRET=your_jwt_secret_here
12-
# CORS_ORIGIN=http://localhost:3000
8+
# Server Configuration
9+
PORT=3001
10+
REST_PORT=3002
11+
GRAPHQL_PORT=3003

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ logs/
88

99
# Coverage files
1010
coverage/
11+
REVIEW_SUMMARY.md
12+
NONPROFIT_SORTING_IMPLEMENTATION.md
13+
.gitignore

package-lock.json

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/graphql/resolvers/nonprofits.resolvers.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,36 @@ import {
22
createNonprofit,
33
createNonprofitSchema,
44
deleteNonprofit,
5-
getAllNonprofits,
65
getNonprofitById,
6+
getNonprofitsWithFilters,
77
updateNonprofit,
88
updateNonprofitSchema,
99
} from '../../../core';
1010
import { GraphQLError } from 'graphql';
1111
import { z } from 'zod';
12+
import type {
13+
StatusType,
14+
NonprofitSortOption,
15+
} from '../../../core/services/nonprofits.service';
1216

1317
// Infer TypeScript types directly from your Zod schemas
1418
type CreateNonprofitInput = z.infer<typeof createNonprofitSchema>;
1519
type UpdateNonprofitInput = z.infer<typeof updateNonprofitSchema>;
1620

21+
interface NonprofitsQueryArgs {
22+
chapterIds?: string[];
23+
statuses?: StatusType[];
24+
sort?: NonprofitSortOption[];
25+
}
26+
1727
export const nonprofitResolvers = {
1828
Query: {
19-
nonprofits: () => getAllNonprofits(),
29+
nonprofits: (
30+
_parent: unknown,
31+
{ chapterIds, statuses, sort }: NonprofitsQueryArgs
32+
) => {
33+
return getNonprofitsWithFilters({ chapterIds, statuses, sort });
34+
},
2035
nonprofit: async (_parent: unknown, { id }: { id: string }) => {
2136
const nonprofit = await getNonprofitById(id);
2237
if (!nonprofit) {
@@ -43,10 +58,13 @@ export const nonprofitResolvers = {
4358
},
4459
updateNonprofit: (
4560
_parent: unknown,
46-
{ id, input }: { id: string; input: UpdateNonprofitInput }
61+
{
62+
nonprofit_id,
63+
input,
64+
}: { nonprofit_id: string; input: UpdateNonprofitInput }
4765
) => {
4866
const validatedInput = updateNonprofitSchema.parse(input);
49-
return updateNonprofit(id, validatedInput);
67+
return updateNonprofit(nonprofit_id, validatedInput);
5068
},
5169
deleteNonprofit: (_parent: unknown, { id }: { id: string }) => {
5270
return deleteNonprofit(id);

src/api/graphql/schemas/nonprofits.schema.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
export const nonprofitSchemaString = `
2+
enum NonprofitSortOption {
3+
A_TO_Z
4+
Z_TO_A
5+
MOST_RECENT
6+
STATUS
7+
}
8+
29
type Nonprofit {
310
nonprofit_id: ID!
411
name: String!
@@ -8,10 +15,15 @@ export const nonprofitSchemaString = `
815
contact_id: String!
916
created_at: String!
1017
updated_at: String!
18+
status: StatusType!
1119
}
1220
1321
type Query {
14-
nonprofits: [Nonprofit!]!
22+
nonprofits(
23+
chapterIds: [ID!]
24+
statuses: [StatusType!]
25+
sort: [NonprofitSortOption!]
26+
): [Nonprofit!]!
1527
nonprofit(id: ID!): Nonprofit
1628
}
1729

src/config/server.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22
import dotenv from 'dotenv';
33
import { z } from 'zod';
44

5+
const envFilePath = (() => {
6+
switch (process.env.NODE_ENV) {
7+
case 'production':
8+
return '.env';
9+
case 'test':
10+
return '.env.test';
11+
default:
12+
return '.env.local';
13+
}
14+
})();
15+
516
dotenv.config({
6-
// path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
7-
// TODO: fix once we get our own test database set up that's not our prod database
8-
path: '.env',
17+
path: envFilePath,
918
});
1019

1120
// Define a schema for your environment variables

src/core/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export * from './services/roles.service';
99
export * from './services/sponsors.service';
1010
export * from './services/volunteer.service';
1111

12+
// Export types for GraphQL resolvers
13+
export type {
14+
StatusType,
15+
NonprofitSortOption,
16+
GetNonprofitsFilters,
17+
EnrichedNonprofit,
18+
} from './services/nonprofits.service';
19+
1220
// Validators
1321
export * from './validators/chapters.validator';
1422
export * from './validators/companies.validator';

0 commit comments

Comments
 (0)