|
| 1 | +# CORD API Development Guidelines |
| 2 | + |
| 3 | +This document provides guidelines for developers working on the CORD API v3 project, a backend built with NestJS, GraphQL, and Neo4j/Gel, using NodeJS and Docker. The API serves the CORD Field front-end. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The CORD API v3 provides [describe functionality briefly, e.g., data management for field operations; replace with specific details if available]. It runs by default on `http://localhost:3000`. For a complete setup guide, see the [cord-docs wiki](https://github.com/SeedCompany/cord-docs/wiki#new-hire-on-boarding). |
| 8 | + |
| 9 | +## Build/Configuration Instructions |
| 10 | + |
| 11 | +### Prerequisites |
| 12 | + |
| 13 | +1. Docker (install from official website, not Homebrew). |
| 14 | +2. NodeJS (version as specified in `package.json`, currently >= 20.6). |
| 15 | + - Check version with `node -v`. Upgrade if compatibility issues arise. |
| 16 | +3. Corepack enabled (`corepack enable`). |
| 17 | +4. Yarn (managed via Corepack). |
| 18 | +5. Gel CLI (`brew install geldata/tap/gel-cli`). |
| 19 | + |
| 20 | +### Setup |
| 21 | + |
| 22 | +1. Install dependencies: |
| 23 | + |
| 24 | + ```bash |
| 25 | + yarn |
| 26 | + ``` |
| 27 | + |
| 28 | +2. Start the Neo4j database using Docker: |
| 29 | + |
| 30 | + ```bash |
| 31 | + docker-compose up -d db |
| 32 | + ``` |
| 33 | + |
| 34 | +3. Set up Gel (next-gen database replacing Neo4j): |
| 35 | + ```bash |
| 36 | + gel project init |
| 37 | + yarn gel:gen |
| 38 | + ``` |
| 39 | + |
| 40 | +### Development |
| 41 | + |
| 42 | +1. Start the development server: |
| 43 | + |
| 44 | + ```bash |
| 45 | + yarn start:dev |
| 46 | + ``` |
| 47 | + |
| 48 | +2. For debugging: |
| 49 | + |
| 50 | + ```bash |
| 51 | + yarn start:debug |
| 52 | + ``` |
| 53 | + |
| 54 | +3. For production: |
| 55 | + |
| 56 | + ```bash |
| 57 | + yarn start:prod |
| 58 | + ``` |
| 59 | + |
| 60 | +4. Generate GraphQL schema: |
| 61 | + |
| 62 | + ```bash |
| 63 | + yarn gel:gen |
| 64 | + ``` |
| 65 | + |
| 66 | +5. Clean build artifacts: |
| 67 | + |
| 68 | + ```bash |
| 69 | + yarn clean |
| 70 | + ``` |
| 71 | + |
| 72 | +6. Create database migrations: |
| 73 | + |
| 74 | + ```bash |
| 75 | + yarn gel:migration |
| 76 | + ``` |
| 77 | + |
| 78 | +7. Generate seed data: |
| 79 | + ```bash |
| 80 | + yarn gel:seed |
| 81 | + ``` |
| 82 | + |
| 83 | +## Testing Information |
| 84 | + |
| 85 | +### Testing Framework |
| 86 | + |
| 87 | +The project uses Jest for testing: |
| 88 | + |
| 89 | +- Unit tests: Located in `tests` folders alongside source code in `src/components`, with `.spec.ts` or `.test.ts` extensions. |
| 90 | +- End-to-end (E2E) tests: Located in `test` directory, with `.e2e-spec.ts` extension. |
| 91 | + |
| 92 | +### Running Tests |
| 93 | + |
| 94 | +1. Run unit tests: |
| 95 | + |
| 96 | + ```bash |
| 97 | + yarn test |
| 98 | + ``` |
| 99 | + |
| 100 | +2. Run E2E tests: |
| 101 | + |
| 102 | + ```bash |
| 103 | + yarn test:e2e |
| 104 | + ``` |
| 105 | + |
| 106 | +3. Run tests with coverage: |
| 107 | + ```bash |
| 108 | + yarn test:cov |
| 109 | + ``` |
| 110 | + |
| 111 | +### Writing Tests |
| 112 | + |
| 113 | +#### Unit Tests |
| 114 | + |
| 115 | +Unit tests should be placed in `tests` folders alongside the service or resolver, with a `.spec.ts` or `.test.ts` extension. For example: |
| 116 | + |
| 117 | +- `src/components/user/user.service.ts` → `src/components/user/tests/user.service.spec.ts` |
| 118 | +- `src/common/util.ts` → `src/common/tests/util.test.ts` |
| 119 | + |
| 120 | +Example unit test: |
| 121 | + |
| 122 | +```typescript |
| 123 | +// src/common/tests/util.test.ts |
| 124 | +import { firstOr } from '../util'; |
| 125 | + |
| 126 | +describe('firstOr', () => { |
| 127 | + it('should return the first item in the array', () => { |
| 128 | + const items = [1, 2, 3]; |
| 129 | + const result = firstOr(items, () => new Error('Array is empty')); |
| 130 | + expect(result).toBe(1); |
| 131 | + }); |
| 132 | + |
| 133 | + it('should throw an error if the array is empty', () => { |
| 134 | + const items: number[] = []; |
| 135 | + expect(() => firstOr(items, () => new Error('Array is empty'))).toThrow( |
| 136 | + 'Array is empty', |
| 137 | + ); |
| 138 | + }); |
| 139 | +}); |
| 140 | +``` |
| 141 | + |
| 142 | +#### E2E Tests |
| 143 | + |
| 144 | +E2E tests should be placed in the `test` directory with an `.e2e-spec.ts` extension, testing GraphQL API endpoints. |
| 145 | + |
| 146 | +Example E2E test: |
| 147 | + |
| 148 | +```typescript |
| 149 | +// test/user.e2e-spec.ts |
| 150 | +import { createTestApp, gql, TestApp } from './utility'; |
| 151 | + |
| 152 | +describe('User API', () => { |
| 153 | + let app: TestApp; |
| 154 | + |
| 155 | + beforeAll(async () => { |
| 156 | + app = await createTestApp(); |
| 157 | + }); |
| 158 | + |
| 159 | + afterAll(async () => { |
| 160 | + await app.close(); |
| 161 | + }); |
| 162 | + |
| 163 | + it('should fetch a user by ID', async () => { |
| 164 | + const result = await app.graphql.query({ |
| 165 | + query: gql` |
| 166 | + query GetUser($id: ID!) { |
| 167 | + user(id: $id) { |
| 168 | + id |
| 169 | + name |
| 170 | + } |
| 171 | + } |
| 172 | + `, |
| 173 | + variables: { id: '123' }, |
| 174 | + }); |
| 175 | + expect(result.user).toEqual({ id: '123', name: 'Alice' }); |
| 176 | + }); |
| 177 | +}); |
| 178 | +``` |
| 179 | + |
| 180 | +## Additional Development Information |
| 181 | + |
| 182 | +### Code Style |
| 183 | + |
| 184 | +The project uses ESLint and Prettier for code formatting and linting: |
| 185 | + |
| 186 | +- Run `yarn lint` to check and fix linting issues. |
| 187 | +- Run `yarn format` to format code using Prettier. |
| 188 | +- Enforce TypeScript strict mode (`tsconfig.json` with `strict: true`). |
| 189 | + |
| 190 | +### GraphQL |
| 191 | + |
| 192 | +The project uses NestJS with GraphQL (graphql-yoga): |
| 193 | + |
| 194 | +- GraphQL schema is generated in `schema.graphql` via `yarn gel:gen`. |
| 195 | +- Resolvers are defined in `*.resolver.ts` files in `src/components`. |
| 196 | +- DTOs (Data Transfer Objects) are defined in `*.dto.ts` files. |
| 197 | +- Only access properties defined in DTOs or interfaces. |
| 198 | + |
| 199 | +### Database |
| 200 | + |
| 201 | +The API is transitioning from Neo4j to Gel: |
| 202 | + |
| 203 | +- Neo4j is used with the `cypher-query-builder` library for queries. |
| 204 | +- Gel is the next-gen database replacing Neo4j. |
| 205 | +- Create migrations with `yarn gel:migration` to update the database schema. |
| 206 | +- Generate seed data with `yarn gel:seed` for testing or development. |
| 207 | + |
| 208 | +### Project Structure |
| 209 | + |
| 210 | +- `src/`: Source code |
| 211 | + - `src/common/`: Common utilities, decorators, and TypeScript interfaces. |
| 212 | + - `src/components/`: Feature-specific modules (e.g., `user`, `order`), containing resolvers, services, DTOs, and tests subfolders. |
| 213 | + - `src/core/`: Core functionality, such as database connections and GraphQL setup. |
| 214 | +- `test/`: E2E tests |
| 215 | + - `test/utility/`: Test utilities (e.g., test app setup). |
| 216 | +- `dbschema/`: Database schema definitions and migrations. |
| 217 | + |
| 218 | +### Coding Standards |
| 219 | + |
| 220 | +- Use camelCase for variables, functions, and methods; ensure names are descriptive. |
| 221 | +- Use PascalCase for classes, interfaces, and enums. |
| 222 | +- Use kebab-case for new folders and files. |
| 223 | +- Use single quotes for strings, 2 spaces for indentation. |
| 224 | +- Prefer async/await for asynchronous operations. |
| 225 | +- Use `const` for constants, minimize `let` usage (e.g., except in try/catch). |
| 226 | +- Use destructuring for objects/arrays, template literals for strings. |
| 227 | +- Follow SOLID principles for modular, reusable, maintainable code. |
| 228 | +- Avoid code duplication, deeply nested statements, hard-coded values. |
| 229 | +- Use constants/enums instead of magic numbers/strings. |
| 230 | +- Avoid mutations: |
| 231 | + - Prefer `const` over `let`. |
| 232 | + - Use spread syntax (e.g., `{ ...object, foo: 'bar' }`) instead of modifying objects. |
| 233 | +- Use strict TypeScript: |
| 234 | + - Define all object shapes in DTOs (`*.dto.ts`) or interfaces in `src/common`. |
| 235 | + - Use type guards for safe property access (e.g., `if ('foo' in obj)`). |
| 236 | + |
| 237 | +### GraphQL Guidelines |
| 238 | + |
| 239 | +- Define resolvers in `*.resolver.ts` with clear input/output types. |
| 240 | +- Use DTOs for input validation and response shaping. |
| 241 | +- Avoid overfetching; include only necessary fields in queries. |
| 242 | +- Handle errors explicitly using NestJS’s error handling (e.g., `throw new BadRequestException()`). |
| 243 | +- Use `// ai example` to mark well-structured resolvers or DTOs. |
| 244 | + |
| 245 | +### Database Guidelines |
| 246 | + |
| 247 | +- Write Cypher queries for Neo4j using `cypher-query-builder` for type safety. |
| 248 | +- Create Gel migrations for schema changes (`yarn gel:migration`). |
| 249 | +- Validate query results against defined DTOs or interfaces. |
| 250 | +- Avoid direct database mutations outside services; encapsulate in `*.service.ts`. |
| 251 | + |
| 252 | +### Version Control |
| 253 | + |
| 254 | +- Create branches: `type/MondayTicketID-description` (e.g., `feature/1234-new-endpoint`). |
| 255 | +- Write commit messages: |
| 256 | + - First line: `[TicketID] - [Short Title] - Imperative verb` (e.g., `0654 - Add user endpoint`). |
| 257 | + - Optional body: Explain why the change is needed. |
| 258 | +- Create PRs for single changes: |
| 259 | + - Link Monday ticket in description. |
| 260 | + - Use bite-sized, logical commits. |
| 261 | + - Demo functionality to reviewers. |
| 262 | + - Check entire PR for applicability. |
| 263 | + - Verify no incorrect property access; properties must match DTOs or interfaces. |
| 264 | + |
| 265 | +### Security Practices |
| 266 | + |
| 267 | +- Sanitize GraphQL inputs to prevent injection attacks. |
| 268 | +- Implement role-based access control (RBAC) in resolvers via GraphQL context. |
| 269 | +- Use environment variables for sensitive data (e.g., database credentials). |
| 270 | +- Configure secure CORS settings in `src/core`. |
| 271 | +- Validate all external data (e.g., API inputs, database results) against DTOs. |
| 272 | +- Check dependencies with Snyk before adding via Yarn. |
| 273 | + |
| 274 | +### Common Errors to Avoid |
| 275 | + |
| 276 | +- **Accessing Non-Existent Properties**: |
| 277 | + - Never assume properties exist without type verification. |
| 278 | + - Reference DTOs in `*.dto.ts` or interfaces in `src/common`. |
| 279 | + - Use type guards (e.g., `if ('foo' in obj)`) or optional chaining (`?.`) for dynamic data. |
| 280 | + - Example (Correct): |
| 281 | + ```typescript |
| 282 | + // ai type-safety Safe property access with type guard |
| 283 | + interface User { |
| 284 | + name?: string; |
| 285 | + } |
| 286 | + function getName(user: User): string { |
| 287 | + return 'name' in user && user.name ? user.name : 'Unknown'; |
| 288 | + } |
| 289 | + ``` |
| 290 | + _Why_: Prevents runtime errors by checking property existence. |
| 291 | + - Example (Incorrect): |
| 292 | + ```typescript |
| 293 | + // ai anti-pattern Assumes non-existent property |
| 294 | + interface User { |
| 295 | + name?: string; |
| 296 | + } |
| 297 | + function getName(user: User): string { |
| 298 | + return user.name; // Error: Property 'name' may be undefined |
| 299 | + } |
| 300 | + ``` |
| 301 | + _Why_: Causes runtime errors due to unverified property access. |
| 302 | +- **Unvalidated GraphQL Inputs**: |
| 303 | + - Always validate inputs using DTOs or class-validator in resolvers. |
| 304 | + - Use `// ai anti-pattern` to mark unvalidated inputs. |
| 305 | + |
| 306 | +### Type Checking |
| 307 | + |
| 308 | +Run `yarn type-check` to check for TypeScript errors without compiling the code. |
| 309 | + |
| 310 | +### Debugging |
| 311 | + |
| 312 | +- Use `yarn start:debug` to start the server in debug mode with breakpoints. |
| 313 | +- Use Jest debugger for tests: `yarn test --selectProjects Unit` with debugger attached. |
| 314 | +- Enable NestJS logging in `src/core` for request/response debugging. |
| 315 | +- Use Neo4j Desktop or Gel CLI (`gel query`) to inspect database state. |
| 316 | + |
| 317 | +### Tagged Comments |
| 318 | + |
| 319 | +- Use `// ai tag` to mark code for reference: |
| 320 | + - `example`: Best practice or model code. |
| 321 | + - `edge-case`: Necessary deviation from standards. |
| 322 | + - `best-practice`: Adherence to coding standards. |
| 323 | + - `anti-pattern`: Code to avoid (pending refactor). |
| 324 | + - `todo`: Needs improvement or refactoring. |
| 325 | + - `workaround`: Temporary fix for a limitation. |
| 326 | + - `performance`: Optimized code. |
| 327 | + - `security`: Security-critical code. |
| 328 | + - `test`: Exemplary test case. |
| 329 | + - `type-safety`: Safe property access. |
| 330 | +- Optionally add notes after the tag (e.g., `// ai example Type-safe resolver`). |
| 331 | +- Search tags with `git grep "ai "` to collect examples. |
| 332 | + |
| 333 | +## Project Structure |
| 334 | + |
| 335 | +- Place GraphQL resolvers in `src/components/*/resolver.ts`. |
| 336 | +- Place services in `src/components/*/service.ts`. |
| 337 | +- Place DTOs in `src/components/*/dto/*.dto.ts`. |
| 338 | +- Place interfaces in `src/common`. |
| 339 | +- Place unit tests in `src/components/*/tests/*.spec.ts`. |
| 340 | +- Place E2E tests in `test/*.e2e-spec.ts`. |
| 341 | + |
| 342 | +## Coding Standards |
| 343 | + |
| 344 | +- Use camelCase for variables/functions, PascalCase for classes/interfaces/DTOs, kebab-case for files/folders. |
| 345 | +- Use single quotes, 2-space indentation, async/await, and `const` over `let`. |
| 346 | +- Use strict TypeScript with DTOs (`*.dto.ts`) or interfaces (`src/common`). |
| 347 | +- Example: |
| 348 | + ```typescript |
| 349 | + // ai type-safety Safe property access |
| 350 | + interface User { |
| 351 | + name?: string; |
| 352 | + } |
| 353 | + function getName(user: User): string { |
| 354 | + return 'name' in user && user.name ? user.name : 'Unknown'; |
| 355 | + } |
| 356 | + ``` |
| 357 | + |
| 358 | +## GraphQL and Database |
| 359 | + |
| 360 | +- Define resolvers in `*.resolver.ts` with DTOs and `class-validator`. |
| 361 | +- Use `cypher-query-builder` for Neo4j queries in `*.service.ts`. |
| 362 | +- Generate schema with `yarn gel:gen`. |
| 363 | +- Example: |
| 364 | + ```typescript |
| 365 | + // ai example Type-safe resolver |
| 366 | + @Resolver(() => UserDto) |
| 367 | + export class UserResolver { |
| 368 | + @Query(() => UserDto) |
| 369 | + async user(@Args('id') id: string): Promise<UserDto> { |
| 370 | + return this.userService.findById(id); |
| 371 | + } |
| 372 | + } |
| 373 | + ``` |
| 374 | + |
| 375 | +## Testing |
| 376 | + |
| 377 | +- Use Jest for tests in `src/components/*/tests/*.spec.ts` (unit) and `test/*.e2e-spec.ts` (E2E). |
| 378 | +- Example: |
| 379 | + ```typescript |
| 380 | + // ai test Unit test |
| 381 | + describe('UserService', () => { |
| 382 | + it('should return user by ID', async () => { |
| 383 | + const result = await userService.findById('123'); |
| 384 | + expect(result).toEqual({ id: '123', name: 'Alice' }); |
| 385 | + }); |
| 386 | + }); |
| 387 | + ``` |
0 commit comments