diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1aaae91..b8f6a03 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,7 +11,8 @@ This repository is a boilerplate for building Node.js applications using TypeScr - **Dependency Injection**: Uses Awilix for managing dependencies, promoting loose coupling and easier testing. - **Event-driven architecture**: Utilizes an event dispatcher for handling domain events, enabling asynchronous processing and decoupling of components. - **Feature-based structure**: Organizes code into features, each encapsulating related functionality, making it easier to navigate and maintain. -- **REST API**: Exposes a RESTful API for client interactions, following standard HTTP methods and status codes. +- **RESTful API**: Exposes a RESTful API for client interactions, following standard HTTP methods and status codes. +- **GraphQL API**: Apollo Server integration for GraphQL queries and mutations alongside REST endpoints. ### Project Structure ```src/ @@ -45,58 +46,110 @@ This repository is a boilerplate for building Node.js applications using TypeScr ### Key Libraries and Functionalities - **TypeScript**: For static typing and modern JavaScript features. - **Express**: Web framework for building REST APIs. -- **TypeORM**: ORM for database interactions. -- **Awilix**: Dependency injection container. -- **@tshio/command-bus**: Command bus for handling commands. -- **@tshio/query-bus**: Query bus for handling queries. +- **Apollo Server**: GraphQL server implementation for building GraphQL APIs. +- **TypeORM**: ORM for database interactions with PostgreSQL. +- **Awilix**: Dependency injection container for managing dependencies. +- **@tshio/command-bus**: Command bus for handling commands in CQRS pattern. +- **@tshio/query-bus**: Query bus for handling queries in CQRS pattern. - **@tshio/event-dispatcher**: Event dispatcher for handling domain events. +- **Celebrate**: Request validation middleware using Joi schemas. +- **Helmet**: Security middleware for setting HTTP headers. +- **CORS**: Cross-origin resource sharing middleware. - `src/shared/pagination-utils`: Utility functions for request parameters to TypeORM options conversion and list endpoint responses. +- `src/shared/cache-decorator`: Caching utilities for query optimization. +- `src/shared/utils`: Common utility functions for various operations. - **UUID v4**: For unique identifiers in database entities. - **Docker**: For containerization and consistent development environment. -- **Jest**: For unit and integration testing. -- **Celebrate**: For input validation in REST endpoints. +- **Mocha + Chai + Supertest**: Testing framework stack for unit and integration tests. +- **SWC**: Fast TypeScript/JavaScript transpiler (alternative to tsc). +- **ESLint + Prettier**: Code quality and formatting tools. +- **Husky**: Git hooks for pre-commit quality checks. ### Key Development Guidelines -#### Important commands -- **Linting**: Use `npm run lint` to check code style. -- **Fixing Lint Issues**: Use `npm run lint-fix` to automatically fix lint issues. -- **Formatting**: Use `npm run format` to format code according to the project's style guide. -- **Running Tests**: Use `npm run integration` for integration tests and `npm run units` for unit tests. -- **Generating Migrations**: Use `npm run generate-migration` to create new migrations. Do not run migrations manually, they are auto-executed at application start. -- **Running migrations**: Use `npm run migrations` to run all pending migrations. +#### Important Commands +- **Development Environment**: Use `npm run watch` to start TypeScript compilation in watch mode and `npm start` to run the API server +- **Building**: Use `npm run build` (tsc) or `npm run build-swc` (SWC transpiler) to compile TypeScript +- **Linting**: Use `npm run lint` to check code style and `npm run lint-fix` to automatically fix issues +- **Formatting**: Use `npm run format` to format code using Prettier +- **Testing**: Use `npm run integration` for integration tests and `npm run units` for unit tests +- **Test Coverage**: Use `npm run integration-tests-coverage` and `npm run services-units-coverage` for coverage reports +- **Database Migrations**: Use `npm run generate-migration` to create new migrations +- **Running Migrations**: Use `npm run run-migrations` to execute pending migrations +- **GraphQL Schema**: Use `npm run generate-schema` to generate GraphQL type definitions +- **Docker Management**: Use `npm run down` to stop all containers and volumes + +#### Development Workflow +1. **Initial Setup**: Run `./init.sh` or manual Docker setup for first-time environment preparation +2. **Development**: Start `npm run watch` for TypeScript compilation and `npm start` for the API server (in separate terminals) +3. **Code Quality**: Run `npm run lint-fix` and `npm run format` before committing changes +4. **Testing**: Execute `npm run integration` and `npm run units` to ensure all tests pass +5. **Database Changes**: Generate migrations with `npm run generate-migration` after model updates +6. **Pre-commit**: Husky hooks automatically run linting and unit tests before git push #### General Guidelines -- Always use TypeScript for type safety and modern JavaScript features. -- Follow the project's coding conventions and architecture patterns. -- Use dependency injection for managing dependencies, avoiding direct instantiation of classes. -- All environment variables has to be extracted and validated using `dotenv` and `joi` packages. Store them in config files like `.env` or `.env.test` and process them in your application startup in `src/config/app.ts`. -- Never use `npm run plop` to generate code, use the provided boilerplate structure and manually create files as needed. -- Never run any commands with `docker-compose` command, use npm scripts instead (e.g., `npm run lint`, `npm run lint-fix`, `npm run forma`). +- Always use TypeScript for type safety and modern JavaScript features +- Follow the project's coding conventions and architectural patterns +- Use dependency injection for managing dependencies, avoiding direct instantiation of classes +- All environment variables must be extracted and validated using `dotenv` and `joi` packages in `src/config/app.ts` +- Store environment configuration in `.env` (local) and `.env.test` (testing), use `.env.dist` as template +- Never use `npm run plop` to generate code, create files manually following the established patterns +- Use Docker containers for all development operations - avoid running commands directly on host machine +- All database migrations are auto-executed at application start, do not run them manually +- Follow the detailed instruction files for specific development patterns: + - Actions: REST endpoint implementation patterns + - Handlers: Command/Query handler implementation + - Models: Entity and repository patterns + - Routing: Route registration and middleware + - Tests: Testing requirements and patterns + - Quality: Code quality standards and tools + - Security: Security best practices and implementation #### Naming Guidelines -- Always think in terms of features, not technical layers. -- Features names should be descriptive and reflect the bounded context (e.g., `planning`, `ordering`, `management`). Avoid naming features by connected entities (e.g., `user`, `product`). -- Use **kebab-case** for directories, filenames and imports (e.g., `user.controller.ts`). -- Use **camelCase** for variables and function names (e.g., `getUserById`). -- Use **PascalCase** for class names and interfaces (e.g., `UserService`). -- use **snake_case** for database columns and TypeORM entity properties (e.g., `first_name`, `last_name`) but use **camelCase** for variable names in TypeScript code. -- Use suffixes like `.handler.ts`, `.command.ts`, `.query.ts`, `.event.ts`, `.subscriber.ts`, `.resolver.ts` for files in features (e.g, `create-user.command.ts`, `user.query.ts`). -- Use suffixes like `Service`, `Repository`, `Controller` for class names (e.g., `UserService`, `UserRepository`, `UserController`). +- Always think in terms of features, not technical layers +- Feature names should be descriptive and reflect the bounded context (e.g., `user-management`, `order-processing`, `notification-service`) +- Avoid naming features by connected entities alone (e.g., avoid just `user`, prefer `user-management`) +- Use **kebab-case** for directories, filenames and imports (e.g., `user-controller.ts`, `create-user.command.ts`) +- Use **camelCase** for variables and function names (e.g., `getUserById`, `processOrder`) +- Use **PascalCase** for class names and interfaces (e.g., `UserService`, `OrderRepository`, `CreateUserCommand`) +- Use **snake_case** for database columns and TypeORM entity properties (e.g., `first_name`, `created_at`) +- Use descriptive suffixes for file types: + - `.action.ts` for REST endpoint handlers + - `.handler.ts` for command/query handlers + - `.command.ts` for command DTOs + - `.query.ts` for query DTOs + - `.event.ts` for domain events + - `.subscriber.ts` for event subscribers + - `.resolver.ts` for GraphQL resolvers + - `.entity.ts` for TypeORM entities + - `.repository.ts` for repository implementations +- Use descriptive suffixes for class names (e.g., `UserService`, `OrderRepository`, `CreateUserHandler`) #### Database Guidelines -- Use **TypeORM** for database interactions. -- Use **UUID v4** for unique identifiers, avoid auto-increment IDs. -- Use repositories for data access in handlers, not direct TypeORM queries. -- Always generate migrations using the provided npm script (`npm run generate-migration`), do not create them manually. -- Never add parameters to the `npm run generate-migration` command, as it will fail. -- All migrations are auto-executed at application start, do not run them manually. -- Migrations are stored in `src/migrations/`. +- Use **TypeORM** for database interactions +- Use **UUID v4** for unique identifiers, avoid auto-increment IDs +- Use repositories for data access in handlers, not direct TypeORM queries +- Always generate migrations using the provided npm script (`npm run generate-migration`), do not create them manually +- Never add parameters to the `npm run generate-migration` command, as it will fail +- All migrations are auto-executed at application start, do not run them manually +- Migrations are stored in `src/migrations/` #### Testing Guidelines -- Every endpoint must have a corresponding integration test. -- Every utility function must have a corresponding unit test. +- Every endpoint must have a corresponding integration test +- Every utility function must have a corresponding unit test #### Security Guidelines -- Never expose sensitive information in error messages or responses. -- Use HTTPS for secure communication, especially in production environments. +- Never expose sensitive information in error messages or responses +- Use HTTPS for secure communication, especially in production environments + +## When Adding Features + +1. Define domain model in appropriate bounded context +2. Create commands/queries with handlers +3. Add repository interfaces and implementations +4. Wire up event subscribers for cross-context communication +5. Register all components in DI container +6. Add validation and error handling +7. Write integration tests following existing patterns + +Focus on maintaining clean boundaries between domain, application, and infrastructure layers while leveraging the established CQRS and event-driven patterns. diff --git a/.github/instructions/actions.instructions.md b/.github/instructions/actions.instructions.md index aee36af..484012b 100644 --- a/.github/instructions/actions.instructions.md +++ b/.github/instructions/actions.instructions.md @@ -2,24 +2,33 @@ applyTo: "**/actions/*.action.ts" --- -- Every action must have a corresponding integration test +# REST API Actions Instructions + +## Purpose +Actions are thin controllers responsible for handling HTTP requests, validating input, and delegating business logic to handlers. They should not contain business logic themselves. + +**CRITICAL REQUIREMENTS**: +- **ALL actions MUST have comprehensive integration tests** in `src/tests/` directory +- Integration tests MUST cover success scenarios, validation errors, and edge cases (see error-handling.instructions.md) +- NO action should be merged without corresponding integration tests + +## Core Guidelines +- Every action must have a corresponding integration test. - Always use `@tshio/command-bus` for command handlers and `@tshio/query-bus` for query handlers. -- GET methods should be used for queries, and POST, PUT, DELETE methods for commands. -- When naming actions files always use names descriptive of the action being performed, for example: - - `create-user.action.ts` for creating a user - - `delete-user.action.ts` for deleting a user - - `get-users.action.ts` for retrieving users -- Use appropriate HTTP status codes for different actions (e.g., 200 for success, 201 for created, 204 for no content). -- For errors use appropriate HTTP status codes (e.g., 400 for bad request, 404 for not found, 500 for internal server error). -- Validation errors are handled by `celebrate` in `src/middleware/error-handler.ts` don't change that -- Add logging for important events and errors using injected logger in handlers and actions. - -- Use `GET` method for list endpoints. -- Use `src/shared/pagination-utils.ts` for creating TypeORM options for list endpoints. -- Always return a JSON response with `meta` and `data` fields. -- The `meta` field should contain pagination, filter, sort, and search information. -- Example request for list endpoint with pagination, sorting, filtering, and search - `GET /users?page=1&limit=10&sort[firstName]=ASC&filter[lastName]=Nowak&search=test` -- Example response for a list endpoint: +- Name action files descriptively (e.g., `create-user.action.ts`, `delete-user.action.ts`, `get-users.action.ts`). +- Use appropriate HTTP status codes (e.g., 200 for success, 201 for created, 204 for no content, 400 for bad request, 404 for not found, 500 for server error). +- Validate input using Celebrate/Joi before processing. +- Delegate business logic to handlers, not actions. +- Add logging for important events and errors using injected logger. +- Document the purpose of each action and expected input/output. + +## List Endpoints +- Use `GET` for list endpoints. +- Use `src/shared/pagination-utils/pagination-utils.ts` for TypeORM options and pagination. +- Always return a JSON response using `makePaginationResult` function. +- Response includes `meta` and `data` fields with pagination, filter, sort, and search info. +- Example request: `GET /users?page=1&limit=10&sort[firstName]=ASC&filter[lastName]=Nowak&search=test` +- Example response: ```json { "meta": { @@ -41,18 +50,14 @@ applyTo: "**/actions/*.action.ts" "data": [{ ... }] } ``` -- Use `filter[field]=value` for filtering by field with specific value. - - Filters on the same field are interpreted as OR: `filter[field1]=value1&filter[field1]=value2` - - Filters on different fields are interpreted as AND: `filter[field1]=value1&filter[field2]=value2` - - Using `%` at the end of value will be interpreted as LIKE query: `filter[field]=value%` -- Use `sort[field]=ASC|DESC` for sorting by field in specified order. -- Use `search=term` for general text search. -- Use `page=1&limit=10` for pagination. -- Use `GET` method for single item endpoints. -- Use singular nouns for single item endpoint paths (e.g., `/users/123e4567-e89b-12d3-a456-426614174000`). +- Use `filter[field]=value` for filtering, `sort[field]=ASC|DESC` for sorting, `search=term` for text search, and `page=1&limit=10` for pagination. + +## Single Item Endpoints +- Use `GET` for single item endpoints. +- Use singular nouns for single item paths (e.g., `/users/123e4567-e89b-12d3-a456-426614174000`). - Always return a JSON response with the item data. -- Example request for single item endpoint - `GET /users/123e4567-e89b-12d3-a456-426614174000` -- Example response for a single item endpoint: +- Example request: `GET /users/123e4567-e89b-12d3-a456-426614174000` +- Example response: ```json { "id": "123e4567-e89b-12d3-a456-426614174000", @@ -61,3 +66,381 @@ applyTo: "**/actions/*.action.ts" "email": "ewa.nowak@example.com" } ``` + +## Example Actions + +### Create Action (Command) +```typescript +import { celebrate, Joi } from "celebrate"; +import { Request, Response } from "express"; +import { CommandBus } from "@tshio/command-bus"; +import { StatusCodes } from "http-status-codes"; +import { CreateUserCommand } from "../commands/create-user.command"; +import { Action } from "../../../../shared/http/types"; + +export const createUserActionValidation = celebrate( + { + headers: Joi.object(), + body: Joi.object({ + name: Joi.string().required(), + email: Joi.string().email().required(), + }), + }, + { abortEarly: false } +); + +export interface CreateUserActionDependencies { + commandBus: CommandBus; +} + +class CreateUserAction implements Action { + constructor(private dependencies: CreateUserActionDependencies) {} + + async invoke(req: Request, res: Response) { + const result = await this.dependencies.commandBus.execute( + new CreateUserCommand({ + name: req.body.name, + email: req.body.email, + }) + ); + + res.status(StatusCodes.CREATED); + res.json(result.result); + } +} + +export default CreateUserAction; +``` + +### Single Item Action (Query) +```typescript +import { celebrate, Joi } from "celebrate"; +import { Request, Response } from "express"; +import { QueryBus } from "@tshio/query-bus"; +import { GetUserQuery } from "../queries/get-user.query"; +import { Action } from "../../../../shared/http/types"; + +export const getUserActionValidation = celebrate( + { + headers: Joi.object(), + params: Joi.object({ + id: Joi.string().uuid().required(), + }), + }, + { abortEarly: false } +); + +export interface GetUserActionDependencies { + queryBus: QueryBus; +} + +class GetUserAction implements Action { + constructor(private dependencies: GetUserActionDependencies) {} + + async invoke(req: Request, res: Response) { + const queryResult = await this.dependencies.queryBus.execute( + new GetUserQuery({ id: req.params.id }) + ); + res.json(queryResult.result); + } +} + +export default GetUserAction; +``` + +### List Action (Query with Pagination) +```typescript +import { celebrate, Joi } from "celebrate"; +import { Request, Response } from "express"; +import { QueryBus } from "@tshio/query-bus"; +import { UsersQuery } from "../queries/users.query"; +import { Action } from "../../../../shared/http/types"; + +export const getUsersActionValidation = celebrate( + { + headers: Joi.object(), + query: Joi.object({ + page: Joi.string().optional(), + limit: Joi.string().optional(), + sort: Joi.object().optional(), + filter: Joi.object().optional(), + search: Joi.string().optional(), + }), + }, + { abortEarly: false } +); + +export interface GetUsersActionDependencies { + queryBus: QueryBus; +} + +class GetUsersAction implements Action { + constructor(private dependencies: GetUsersActionDependencies) {} + + async invoke(req: Request, res: Response) { + const queryResult = await this.dependencies.queryBus.execute( + new UsersQuery({ + ...req.query, + page: Number(req.query.page), + limit: Number(req.query.limit), + }) + ); + res.json(queryResult.result); + } +} + +export default GetUsersAction; +``` + +### Update Action (Command) +```typescript +import { celebrate, Joi } from "celebrate"; +import { Request, Response } from "express"; +import { CommandBus } from "@tshio/command-bus"; +import { UpdateUserCommand } from "../commands/update-user.command"; +import { Action } from "../../../../shared/http/types"; + +export const updateUserActionValidation = celebrate( + { + headers: Joi.object(), + params: Joi.object({ + id: Joi.string().uuid().required(), + }), + body: Joi.object({ + name: Joi.string().optional(), + email: Joi.string().email().optional(), + }), + }, + { abortEarly: false } +); + +export interface UpdateUserActionDependencies { + commandBus: CommandBus; +} + +class UpdateUserAction implements Action { + constructor(private dependencies: UpdateUserActionDependencies) {} + + async invoke(req: Request, res: Response) { + const result = await this.dependencies.commandBus.execute( + new UpdateUserCommand({ + id: req.params.id, + ...req.body, + }) + ); + res.json(result.result); + } +} + +export default UpdateUserAction; +``` + +### Delete Action (Command) +```typescript +import { celebrate, Joi } from "celebrate"; +import { Request, Response } from "express"; +import { CommandBus } from "@tshio/command-bus"; +import { StatusCodes } from "http-status-codes"; +import { DeleteUserCommand } from "../commands/delete-user.command"; +import { Action } from "../../../../shared/http/types"; + +export const deleteUserActionValidation = celebrate( + { + headers: Joi.object(), + params: Joi.object({ + id: Joi.string().uuid().required(), + }), + }, + { abortEarly: false } +); + +export interface DeleteUserActionDependencies { + commandBus: CommandBus; +} + +class DeleteUserAction implements Action { + constructor(private dependencies: DeleteUserActionDependencies) {} + + async invoke(req: Request, res: Response) { + await this.dependencies.commandBus.execute( + new DeleteUserCommand({ id: req.params.id }) + ); + res.status(StatusCodes.NO_CONTENT).send(); + } +} + +export default DeleteUserAction; +``` + +## Advanced Patterns + +### Input Sanitization and Transformation +Use Joi custom methods for input transformation: + +```typescript +export const createUserActionValidation = celebrate( + { + body: Joi.object({ + email: Joi.string().email().lowercase().trim().required(), + name: Joi.string().trim().min(2).max(50).required(), + }), + }, + { abortEarly: false } +); +``` + +### Complex Filtering for List Endpoints +For advanced filtering, extend validation patterns: + +```typescript +export const getUsersActionValidation = celebrate( + { + query: Joi.object({ + // Pagination + page: Joi.string().optional(), + limit: Joi.string().optional(), + + // Sorting - single or multiple fields + sort: Joi.object().pattern( + Joi.string(), + Joi.string().valid('ASC', 'DESC') + ).optional(), + + // Filtering - exact matches + filter: Joi.object().pattern( + Joi.string(), + Joi.alternatives().try( + Joi.string(), + Joi.array().items(Joi.string()) + ) + ).optional(), + + // Text search across multiple fields + search: Joi.string().min(3).optional(), + }), + }, + { abortEarly: false } +); +``` + +## Testing Requirements + +**MANDATORY**: Every action MUST have comprehensive integration tests. + +### Integration Test Structure +Create tests in `src/tests/` directory following this pattern: + +```typescript +import { expect } from "chai"; +import "mocha"; +import request from "supertest"; + +describe("/api/users integration", () => { + beforeEach(async () => { + // Clear database before each test + // Use your database clearing utility + }); + + describe("POST /api/users", () => { + it("should create user with valid data", async () => { + const userData = { + name: "John Doe", + email: "john.doe@example.com" + }; + + return request(await global.container.cradle.app) + .post("/api/users") + .send(userData) + .expect(201) + .then((response) => { + expect(response.body).to.have.property("id"); + expect(response.body.name).to.equal(userData.name); + expect(response.body.email).to.equal(userData.email); + }); + }); + + it("should return 400 for invalid email", async () => { + const userData = { + name: "John Doe", + email: "invalid-email" + }; + + return request(await global.container.cradle.app) + .post("/api/users") + .send(userData) + .expect(400); + }); + + it("should return 400 for missing required fields", async () => { + return request(await global.container.cradle.app) + .post("/api/users") + .send({}) + .expect(400); + }); + }); + + describe("GET /api/users", () => { + it("should return paginated users list", async () => { + return request(await global.container.cradle.app) + .get("/api/users?page=1&limit=10") + .expect(200) + .then((response) => { + expect(response.body).to.have.property("meta"); + expect(response.body).to.have.property("data"); + expect(response.body.meta).to.have.property("pagination"); + }); + }); + + it("should filter users by search term", async () => { + return request(await global.container.cradle.app) + .get("/api/users?search=john") + .expect(200); + }); + }); + + describe("GET /api/users/:id", () => { + it("should return specific user", async () => { + // First create a user, then fetch it + const createResponse = await request(await global.container.cradle.app) + .post("/api/users") + .send({ name: "Test User", email: "test@example.com" }); + + return request(await global.container.cradle.app) + .get(`/api/users/${createResponse.body.id}`) + .expect(200) + .then((response) => { + expect(response.body.id).to.equal(createResponse.body.id); + }); + }); + + it("should return 404 for non-existent user", async () => { + const nonExistentId = "550e8400-e29b-41d4-a716-446655440000"; + return request(await global.container.cradle.app) + .get(`/api/users/${nonExistentId}`) + .expect(404); + }); + }); +}); +``` + +### Test Coverage Requirements +Your tests MUST cover: +- ✅ Success scenarios for all endpoints +- ✅ Validation errors (400 responses) +- ✅ Not found errors (404 responses) +- ✅ Authentication/authorization errors (if applicable) +- ✅ Edge cases and boundary conditions +- ✅ Pagination and filtering functionality + +## Best Practices +- Keep actions minimal and focused on request/response logic. +- Always validate input before processing. +- Use dependency injection to resolve handlers. +- Throw errors directly; rely on global error handling. +- Use consistent response formats across all endpoints. +- Follow RESTful conventions for HTTP methods and status codes. +- Include comprehensive integration tests for every action. +- Use appropriate HTTP status codes from `http-status-codes` package. + +## GraphQL Actions +If your project supports GraphQL, resolvers should follow similar principles: validate input, delegate to handlers, and format responses. diff --git a/.github/instructions/commands-queries.instructions.md b/.github/instructions/commands-queries.instructions.md new file mode 100644 index 0000000..771d2d4 --- /dev/null +++ b/.github/instructions/commands-queries.instructions.md @@ -0,0 +1,331 @@ +--- +applyTo: "**/+(commands|queries)/*.+(command|query).ts" +--- + +# Commands and Queries Instructions + +## Overview +Commands and Queries are the foundation of the CQRS (Command Query Responsibility Segregation) pattern in this application. They serve as data transfer objects (DTOs) that carry information between different layers of the application. + +**CRITICAL REQUIREMENTS**: +- Commands handle state-changing operations (POST, PUT, DELETE) +- Queries handle data retrieval operations (GET) +- All commands and queries must have a unique type identifier +- Use TypeScript interfaces for payload structure +- Follow consistent naming conventions + +## Command Structure + +### Basic Command Pattern +Commands represent intentions to change application state: + +```typescript +import { Command } from "@tshio/command-bus"; + +export const CREATE_USER_COMMAND_TYPE = "users/CREATE_USER"; + +export interface CreateUserCommandPayload { + firstName: string; + lastName: string; + email: string; + role?: string; +} + +export class CreateUserCommand implements Command { + public type: string = CREATE_USER_COMMAND_TYPE; + + constructor(public payload: CreateUserCommandPayload) {} +} +``` + +### Command Naming Conventions +- **Type constant**: `FEATURE_ACTION_COMMAND_TYPE` pattern +- **Interface**: `{Action}CommandPayload` +- **Class**: `{Action}Command` +- **File**: `{action}.command.ts` + +By {action}, we mean a verb that describes the operation, e.g., CreateUser, UpdateOrder, DeleteProduct. + +Examples: +```typescript +// User management commands +export const CREATE_USER_COMMAND_TYPE = "users/CREATE_USER"; +export const UPDATE_USER_COMMAND_TYPE = "users/UPDATE_USER"; +export const DELETE_USER_COMMAND_TYPE = "users/DELETE_USER"; +export const ACTIVATE_USER_COMMAND_TYPE = "users/ACTIVATE_USER"; + +// Order management commands +export const CREATE_ORDER_COMMAND_TYPE = "orders/CREATE_ORDER"; +export const PROCESS_ORDER_COMMAND_TYPE = "orders/PROCESS_ORDER"; +export const CANCEL_ORDER_COMMAND_TYPE = "orders/CANCEL_ORDER"; +``` + +### Command Payload Patterns + +#### Create Commands +```typescript +// create-user.command.ts +export const CREATE_USER_COMMAND_TYPE = "users/CREATE_USER"; + +export interface CreateUserCommandPayload { + firstName: string; + lastName: string; + email: string; + role: UserRole; + preferences?: UserPreferences; +} + +export class CreateUserCommand implements Command { + public type: string = CREATE_USER_COMMAND_TYPE; + + constructor(public payload: CreateUserCommandPayload) {} +} +``` + +#### Update Commands +```typescript +// update-user.command.ts +export const UPDATE_USER_COMMAND_TYPE = "users/UPDATE_USER"; + +export interface UpdateUserCommandPayload { + id: string; // Always required for updates + firstName?: string; // Optional fields for partial updates + lastName?: string; + email?: string; + status?: UserStatus; + updatedBy: string; // Audit information +} + +export class UpdateUserCommand implements Command { + public type: string = UPDATE_USER_COMMAND_TYPE; + + constructor(public payload: UpdateUserCommandPayload) {} +} +``` + +#### Delete Commands +```typescript +// delete-user.command.ts +export const DELETE_USER_COMMAND_TYPE = "users/DELETE_USER"; + +export interface DeleteUserCommandPayload { + id: string; + deletedBy: string; // Audit information + reason?: string; // Optional deletion reason +} + +export class DeleteUserCommand implements Command { + public type: string = DELETE_USER_COMMAND_TYPE; + + constructor(public payload: DeleteUserCommandPayload) {} +} +``` + +#### Business Logic Commands +```typescript +// activate-user.command.ts +export const ACTIVATE_USER_COMMAND_TYPE = "users/ACTIVATE_USER"; + +export interface ActivateUserCommandPayload { + id: string; + activatedBy: string; + activationReason?: string; +} + +export class ActivateUserCommand implements Command { + public type: string = ACTIVATE_USER_COMMAND_TYPE; + + constructor(public payload: ActivateUserCommandPayload) {} +} +``` + +#### Complex Business Commands +```typescript +// process-order.command.ts +export const PROCESS_ORDER_COMMAND_TYPE = "orders/PROCESS_ORDER"; + +export interface ProcessOrderCommandPayload { + orderId: string; + items: OrderItem[]; + shippingAddress: Address; + paymentMethod: PaymentMethod; + couponCode?: string; + specialInstructions?: string; +} + +export interface OrderItem { + productId: string; + quantity: number; + price: number; + customization?: ProductCustomization; +} + +export class ProcessOrderCommand implements Command { + public type: string = PROCESS_ORDER_COMMAND_TYPE; + + constructor(public payload: ProcessOrderCommandPayload) {} +} +``` + +## Query Structure + +### Basic Query Pattern +Queries represent requests for data retrieval: + +```typescript +import { Query } from "@tshio/query-bus"; + +export const USERS_QUERY_TYPE = "users/USERS"; + +export interface UsersQueryPayload { + page?: number; + limit?: number; + sort?: Record; + filter?: UserFilters; + search?: string; +} + +export class UsersQuery implements Query { + public type: string = USERS_QUERY_TYPE; + + constructor(public payload: UsersQueryPayload) {} +} +``` + +### Query Result Classes +Define structured result classes for complex queries: + +```typescript +export class UsersQueryResult { + constructor( + public readonly data: PaginationResult + ) {} +} + +export class GetUserQueryResult { + constructor( + public readonly user: UserEntity, + public readonly permissions: UserPermissions + ) {} +} +``` + +### Query Payload Patterns + +#### List Queries with Pagination +```typescript +// users.query.ts +import { PaginationParamsDto } from "../../../shared/pagination-utils/pagination-utils"; + +export const USERS_QUERY_TYPE = "users/USERS"; + +export interface UsersQueryPayload extends PaginationParamsDto { + search?: string; + filter?: { + status?: UserStatus[]; + role?: UserRole[]; + createdAfter?: Date; + createdBefore?: Date; + }; + sort?: { + firstName?: 'ASC' | 'DESC'; + lastName?: 'ASC' | 'DESC'; + createdAt?: 'ASC' | 'DESC'; + email?: 'ASC' | 'DESC'; + }; +} + +export class UsersQuery implements Query { + public type: string = USERS_QUERY_TYPE; + + constructor(public payload: UsersQueryPayload) {} +} + +export class UsersQueryResult { + constructor( + public readonly data: PaginationResult + ) {} +} +``` + +#### Single Item Queries +```typescript +// get-user.query.ts +export const GET_USER_QUERY_TYPE = "users/GET_USER"; + +export interface GetUserQueryPayload { + id: string; + includePermissions?: boolean; + includeProfile?: boolean; + includeOrders?: boolean; +} + +export class GetUserQuery implements Query { + public type: string = GET_USER_QUERY_TYPE; + + constructor(public payload: GetUserQueryPayload) {} +} + +export class GetUserQueryResult { + constructor( + public readonly user: UserEntity, + public readonly permissions?: UserPermissions + ) {} +} +``` + +#### Complex Search Queries +```typescript +// search-products.query.ts +export const SEARCH_PRODUCTS_QUERY_TYPE = "products/SEARCH_PRODUCTS"; + +export interface SearchProductsQueryPayload { + query: string; + category?: string[]; + priceRange?: { + min: number; + max: number; + }; + inStock?: boolean; + brand?: string[]; + location?: { + latitude: number; + longitude: number; + radius: number; + }; + page?: number; + limit?: number; +} + +export class SearchProductsQuery implements Query { + public type: string = SEARCH_PRODUCTS_QUERY_TYPE; + + constructor(public payload: SearchProductsQueryPayload) {} +} +``` + +## Best Practices + +### Command Guidelines +- **Immutable**: Never modify command payload after creation +- **Intention revealing**: Command names should describe the business intention +- **Single responsibility**: One command should handle one business operation + +### Query Guidelines +- **Specific**: Design queries for specific use cases rather than generic data access +- **Cacheable**: Structure queries to enable effective caching +- **Pagination**: Always include pagination for list queries +- **Filtering**: Provide flexible filtering options + +### Naming Conventions +- **Commands**: Use imperative verbs (CreateUser, UpdateOrder, CancelPayment) +- **Queries**: Use descriptive nouns (Users, GetUserDetails, SearchProducts) +- **Types**: Use namespace/action pattern (users/CREATE_USER, orders/SEARCH) + +### Data Transfer +- **Type safety**: Use TypeScript interfaces for all payloads +- **Serializable**: Ensure all payload data is JSON serializable +- **No business logic**: Keep commands/queries as pure data containers and leave logic to handlers +- **Documentation**: Document complex payload structures with examples + +This structure ensures clean separation between HTTP concerns (actions), business logic (handlers), and data transfer (commands/queries) while maintaining type safety and clear data flow throughout the application. \ No newline at end of file diff --git a/.github/instructions/error-handling.instructions.md b/.github/instructions/error-handling.instructions.md new file mode 100644 index 0000000..79db4f8 --- /dev/null +++ b/.github/instructions/error-handling.instructions.md @@ -0,0 +1,278 @@ +--- +applyTo: "**/*.{ts,js}" +--- + +# Error Handling Instructions + +## Overview +This application implements a comprehensive error handling strategy with global error handling middleware, structured error responses, and consistent error patterns across all layers. All errors flow through a centralized error handler to ensure consistent format and proper logging. + +**CRITICAL REQUIREMENTS**: +- **ALL errors must flow through global error handler** in `src/middleware/error-handler.ts` +- Use predefined error classes for consistent error handling +- Never expose sensitive information in error messages +- All errors are logged using @tshio/logger with appropriate levels +- Maintain consistent JSON error response format + +## Error Architecture + +### Global Error Handler +All errors are processed through `src/middleware/error-handler.ts` which: +- Catches all thrown errors from actions, handlers, and middleware +- Transforms errors into consistent JSON responses +- Logs errors with appropriate severity levels +- Prevents sensitive information exposure +- Returns proper HTTP status codes + +### Error Flow Pattern +``` +Action/Handler/Middleware → Throws Error → Global Error Handler → JSON Response +``` + +## Error Classes and Usage + +### Built-in Error Classes +Use error classes from `src/errors/` for consistent error handling: + +```typescript +import { NotFoundError } from "../../../../shared/errors/not-found.error"; +import { HttpError } from "../../../../shared/errors/http.error"; +import { AppError } from "../../../../shared/errors/app.error"; +``` + +### Error Class Guidelines + +#### NotFoundError (404 Not Found) +Use for resources that don't exist: +```typescript +// In handlers +const user = await this.dependencies.userRepository.findOneBy({ + id: command.payload.id +}); + +if (!user) { + throw new NotFoundError("User not found"); +} +``` + +#### HttpError (Custom HTTP Status) +Use for business logic violations with specific HTTP status codes: +```typescript +// Business logic validation +if (user.status === 'deleted') { + throw new HttpError("Cannot update deleted user", 400); +} + +// Authorization errors +if (!user.canAccess(resource)) { + throw new HttpError("Access forbidden", 403); +} + +// Conflict errors +if (emailAlreadyExists) { + throw new HttpError("Email already in use", 409); +} +``` + +#### AppError (Internal Application Errors) +Reserved for internal application errors: +```typescript +// System-level errors +throw new AppError("Database connection failed"); +``` + +## Error Handling by Layer + +### Actions +Actions should throw errors directly and let the global error handler process them: + +```typescript +export default class CreateUserAction implements Action { + async invoke(req: Request, res: Response) { + // Validation is handled by Celebrate middleware + + const result = await this.dependencies.commandBus.execute( + new CreateUserCommand({ + name: req.body.name, + email: req.body.email, + }) + ); + + // No try/catch needed - global error handler catches thrown errors + res.status(StatusCodes.CREATED); + res.json(result.result); + } +} +``` + +#### Action Error Guidelines +- **Never use try/catch blocks** - let errors bubble up to global handler +- **Never return error responses manually** - throw errors instead +- **Focus on happy path logic** - error handling is automatic +- **Use appropriate error classes** for different scenarios + +### Handlers (Business Logic) +Handlers contain business logic validation and should throw appropriate errors: + +```typescript +export default class CreateUserHandler implements CommandHandler { + async execute(command: CreateUserCommand) { + this.dependencies.logger.info("Command CreateUser executed"); + + const { name, email } = command.payload; + + const existingUser = await this.dependencies.userRepository.findOneBy({ email }); + + if (!existingUser) { + throw new HttpError("User not found", 404); + } + + const userEntity = this.dependencies.userRepository.create({ name, email }); + const savedUser = await this.dependencies.userRepository.save(userEntity); + + return { result: savedUser }; + } +} +``` + +#### Handler Error Guidelines +- **Throw errors as exceptions**, not return them as responses +- **Use NotFoundError** for 404 errors +- **Use HttpError** for custom HTTP errors with specific status codes +- **Include meaningful error messages** for debugging +- **Log important business rule violations** if needed + +## Input Validation + +### Celebrate/Joi Validation +Input validation errors are automatically handled by Celebrate middleware: + +```typescript +export const createUserActionValidation = celebrate( + { + body: Joi.object({ + email: Joi.string().email().required(), + name: Joi.string().trim().min(2).max(50).required(), + }), + }, + { abortEarly: false } +); +``` + +### Validation Error Flow +1. Celebrate validates input against Joi schema +2. Validation failures are caught by global error handler +3. Proper 400 status with validation details returned +4. No manual validation error handling needed + +### Business Logic Validation +Separate from input validation, business rules should be validated in handlers: + +```typescript +// In handlers - business logic validation +if (user.orders.length > 0) { + throw new HttpError("Cannot delete user with existing orders", 409); +} +``` + +## Security Considerations + +### Sensitive Information Protection +- **Never expose database details** in error messages +- **Don't reveal internal system information** in responses +- **Use generic messages for authentication/authorization failures** +- **Log detailed errors server-side only** + +### Secure Error Examples +```typescript +// Good - Generic message for production +if (!validCredentials) { + throw new HttpError("Invalid credentials", 401); +} + +// Bad - Exposes sensitive information +if (!user) { + throw new Error("User with email john@example.com not found in users table"); +} +``` + +## Testing Error Scenarios + +### Integration Tests for Error Handling +Test error scenarios through API endpoints: + +```typescript +describe("Error Handling", () => { + it("should return 400 for validation errors", async () => { + const response = await request(app) + .post("/api/users") + .send({ invalid: "data" }) + .expect(400); + + expect(response.body).to.have.property("error"); + expect(response.body.error).to.include("validation"); + }); + + it("should return 404 for non-existent resources", async () => { + const nonExistentId = "550e8400-e29b-41d4-a716-446655440000"; + await request(app) + .get(`/api/users/${nonExistentId}`) + .expect(404); + }); + + it("should handle business rule violations", async () => { + const response = await request(app) + .post("/api/users") + .send({ email: "existing@example.com" }) + .expect(409); + + expect(response.body.error.message).to.include("already exists"); + }); +}); +``` + +### Unit Tests for Error Scenarios +Test error handling in utility functions: + +```typescript +describe("Utility Function", () => { + it("should throw error for invalid input", () => { + expect(() => functionToTest(null)).to.throw("Expected error message"); + }); + + it("should handle edge cases gracefully", () => { + expect(() => functionToTest("")).to.throw("Invalid input"); + }); +}); +``` + +### Required Error Test Coverage +- **Validation errors**: Test all input validation rules and error responses +- **Authentication errors**: Test protected endpoints with and without valid tokens +- **Authorization errors**: Test role-based access control where applicable +- **Not found errors**: Test non-existent resource access +- **Business rule violations**: Test all business logic error scenarios +- **Edge cases**: Test boundary conditions and edge cases + +## Best Practices + +### Error Handling Guidelines +- **Single source of truth**: All errors flow through global error handler +- **Fail fast**: Validate and throw errors as early as possible +- **Meaningful messages**: Use descriptive error messages for debugging +- **Consistent format**: Use predefined error classes for consistency +- **Security first**: Never expose sensitive information in error responses + +### Development Workflow +1. **Design error scenarios** when implementing features +2. **Use appropriate error classes** for different types of failures +3. **Test error paths** alongside success paths +4. **Monitor error logs** in development and production +5. **Refine error messages** based on user feedback and debugging needs + +### Common Anti-patterns to Avoid +- **Manual error response creation** instead of throwing errors +- **Try/catch blocks in actions** that swallow errors +- **Returning error objects** instead of throwing exceptions +- **Exposing internal system details** in error messages +- **Inconsistent error response formats** across endpoints diff --git a/.github/instructions/handlers.instructions.md b/.github/instructions/handlers.instructions.md index 099096f..726298f 100644 --- a/.github/instructions/handlers.instructions.md +++ b/.github/instructions/handlers.instructions.md @@ -1,51 +1,248 @@ --- -applyTo: "**/*.handler.ts" +applyTo: "**/(handlers|query-handlers)/*.handler.ts" --- -- All handlers must have a `.handler.ts` suffix. -- Always use single parameter in constructor for dependencies. For example: +# Command and Query Handlers Instructions + +## Overview +Handlers contain the business logic of your application following CQRS (Command Query Responsibility Segregation) pattern. They are divided into two types: +- **Command Handlers**: Handle state-changing operations (POST, PUT, DELETE) +- **Query Handlers**: Handle data retrieval operations (GET) + +**CRITICAL REQUIREMENTS**: +- **ALL handlers are tested through REST API integration tests** (not unit tests) +- Handlers contain business logic, while actions handle HTTP concerns +- Never use Command Bus or Query Bus directly in handlers +- Always use dependency injection pattern + +## Command Handlers + +Command handlers process state-changing operations and follow this pattern: + +### Command Handler Structure ```typescript -import { UserService } from "../services/user.service"; +import { CommandHandler } from "@tshio/command-bus"; +import { Logger } from "@tshio/logger"; +import { Repository } from "typeorm"; +import { CREATE_USER_COMMAND_TYPE, CreateUserCommand } from "../commands/create-user.command"; +import { UserEntity } from "../models/user.entity"; +import { StatusCodes } from "http-status-codes"; -interface UserHandlerDependencies { - userService: UserService; +export interface CreateUserHandlerDependencies { + logger: Logger; + userRepository: Repository; } -export class UserHandler { - constructor(private dependencies: UserHandlerDependencies) {} +export default class CreateUserHandler implements CommandHandler { + public commandType: string = CREATE_USER_COMMAND_TYPE; - async handle(request: Request, response: Response): Promise { - const user = await this.dependencies.userService.getUserById(request.params.id); - response.json(user); + constructor(private dependencies: CreateUserHandlerDependencies) {} + + async execute(command: CreateUserCommand) { + this.dependencies.logger.info("Command CreateUser executed"); + + const { name, email } = command.payload; + + // Business logic and validation + const existingUser = await this.dependencies.userRepository.findOneBy({ email }); + if (existingUser) { + throw new HttpException("User with this email already exists", StatusCodes.BAD_REQUEST); + } + + // Create and save entity + const userEntity = this.dependencies.userRepository.create({ + name, + email, + }); + + const savedUser = await this.dependencies.userRepository.save(userEntity); + + return { + result: savedUser, + }; } } ``` -- Never use Command Bus or Query Bus directly in handlers. Instead, dispatch events using the provider `@tshio/event-dispatcher` -- Always inject repositories and not data sources directly into handlers: + +### Command Handler Guidelines +- Always implement `CommandHandler` interface +- Include `commandType` property matching the command type +- Use constructor dependency injection with dependencies interface +- Include logger in dependencies for operation tracking +- Inject repositories, not data sources directly +- Contain business logic and validation rules +- Return result in `{ result: T }` format +- Use global error handling for business rule violations and validation failures + +## Query Handlers + +Query handlers process data retrieval operations and follow this pattern: + +### Query Handler Structure ```typescript +import { Like, Repository } from "typeorm"; +import { QueryHandler } from "@tshio/query-bus"; +import { CacheQuery } from "../../../../shared/cache-decorator"; +import { USERS_QUERY_TYPE, UsersQuery, UsersQueryResult } from "../queries/users.query"; import { UserEntity } from "../models/user.entity"; -import { Repository } from "typeorm"; +import { createFindManyOptions, makePaginationResult } from "../../../../shared/pagination-utils/pagination-utils"; -interface UserHandlerDependencies { +export interface UsersQueryHandlerDependencies { userRepository: Repository; } -export class UserHandler { - constructor(private dependencies: UserHandlerDependencies) {} +export default class UsersQueryHandler implements QueryHandler { + public queryType: string = USERS_QUERY_TYPE; + + constructor(private dependencies: UsersQueryHandlerDependencies) {} + + @CacheQuery({ duration: 300 }) // Cache for 5 minutes + async execute(query: UsersQuery): Promise { + const { payload } = query; + + // Use pagination utilities + const findOptions = createFindManyOptions(this.dependencies.userRepository, payload); + + // Add search functionality + if (payload.search) { + findOptions.where = { + ...findOptions.where, + email: Like(`%${payload.search}%`) + }; + } + + const [data, total] = await this.dependencies.userRepository.findAndCount(findOptions); + + return new UsersQueryResult( + makePaginationResult(data, total, findOptions, payload.search) + ); + } +} +``` + +### Query Handler Guidelines +- Always implement `QueryHandler` interface +- Include `queryType` property matching the query type +- Use constructor dependency injection with dependencies interface +- Use `@CacheQuery` decorator for cacheable queries +- Leverage `createFindManyOptions` and `makePaginationResult` for list queries +- Return structured result using result classes +- Never modify state in query handlers + +## Dependency Injection Pattern + +### Dependencies Interface +Always define a dependencies interface for type safety: + +```typescript +export interface CreateUserHandlerDependencies { + logger: Logger; // Always required for commands + userRepository: Repository; // Repository injection + emailService?: EmailService; // Optional services +} +``` + +## Advanced Patterns + +### Business Logic Organization +Structure complex business operations clearly: + +```typescript +async execute(command: CreateOrderCommand) { + this.dependencies.logger.info("Creating order", { userId: command.payload.userId }); - async handle(request: Request, response: Response): Promise { - const user = await this.dependencies.userRepository.findOneBy({ id: request.params.id }); - response.json(user); + // 1. Validation + await this.validateOrderCreation(command.payload); + + // 2. Business logic + const order = await this.createOrderEntity(command.payload); + + // 3. Side effects + await this.processOrderSideEffects(order); + + return { result: order }; +} + +private async validateOrderCreation(payload: CreateOrderPayload) { + const user = await this.dependencies.userRepository.findOneBy({ id: payload.userId }); + if (!user) { + throw new NotFoundError("User not found"); } } ``` -- Never create tests for handlers. -- Don't use `@CacheQuery` decorator in any handlers unless explicitly stated otherwise. -- Erorrs should be thrown as exceptions, not returned as responses. They will be handled by the global error handler. -- There are built-in error classes in `src/shared/errors` that you can use to throw errors with appropriate HTTP status codes and messages. -- Use HttpError classes from `src/shared/errors` for throwing HTTP errors. -- The AppError shouldn't be used in handlers, as it is a generic error class for internal errors. -- The NotFoundError should be used for 404 errors. -- The Validation Errors are handled by `celebrate` in `src/middleware/error-handler.ts`, so you don't need to handle them in handlers. -- In case you need to handle specific errors, you can create custom error classes in `src/shared/errors` and throw them in handlers, but remember for them to extend the `AppError` class. -- Add logging for important events and errors using injected logger in handlers and actions. \ No newline at end of file + +## Testing Strategy + +### Integration Testing +Handlers are tested through REST API integration tests, not unit tests: + +```typescript +// Test handlers indirectly through actions +describe("POST /api/users", () => { + it("should create user successfully", async () => { + // This tests CreateUserHandler through CreateUserAction + const response = await request(app) + .post("/api/users") + .send({ name: "John Doe", email: "john@example.com" }) + .expect(201); + + expect(response.body).to.have.property("id"); + }); +}); +``` + +### Why No Unit Tests for Handlers +- Handlers contain business logic that should be tested in realistic scenarios +- Integration tests provide better coverage of actual application behavior +- Database interactions are better tested with real database connections +- Mocking repositories often creates tests that don't reflect real issues + +## Container Registration + +Register handlers in the appropriate container files: + +### Command Handlers +```typescript +// src/container/command-handlers.ts +import CreateUserHandler from "../app/features/users/handlers/create-user.handler"; + +export const registerCommandHandlers = (): NameAndRegistrationPair[] => [ + // ... other handlers + asClass(CreateUserHandler), +]; +``` + +### Query Handlers +```typescript +// src/container/query-handlers.ts +import UsersQueryHandler from "../app/features/users/query-handlers/users.query.handler"; + +export const registerQueryHandlers = (): NameAndRegistrationPair[] => [ + // ... other handlers + asClass(UsersQueryHandler), +]; +``` + +## Best Practices + +### Command Handlers +- Always include logger in dependencies for operation tracking +- Implement comprehensive business logic validation +- Use transactions for multi-entity operations +- Return meaningful results for actions to use +- Keep handlers focused on single responsibility + +### Query Handlers +- Use `@CacheQuery` decorator for expensive or frequently accessed queries +- Leverage pagination utilities for list queries +- Optimize database queries for performance +- Structure complex queries using QueryBuilder +- Return structured results using result classes + +### General Guidelines +- All handlers must have `.handler.ts` suffix +- Use dependency injection with typed interfaces +- Inject repositories, not data sources directly +- Throw errors for business rule violations +- Add comprehensive logging for debugging +- Follow CQRS principles strictly (commands change state, queries don't) \ No newline at end of file diff --git a/.github/instructions/models.instructions.md b/.github/instructions/models.instructions.md index 3d6feaa..79b0c4e 100644 --- a/.github/instructions/models.instructions.md +++ b/.github/instructions/models.instructions.md @@ -1,28 +1,32 @@ --- -applyTo: "**/+(*.entity.ts|*.repository.ts)" +applyTo: "**/*.repository.ts" --- -- All entities must have a `.entity.ts` suffix. -- Always register repository for each entity in `src/container/database.ts`. -- When registering a repository, use this approach: -```typescript -userRepository: asValue(dbDataSource.getRepository(UserEntity)), -``` -- Use **UUID v4** for unique identifiers, avoid auto-increment IDs. -- Use `@PrimaryColumn()` for UUID columns, not `@PrimaryGeneratedColumn()`. -- Always generate migrations using the provided npm script (`npm run generate-migration`), do not create them manually. -- Never add parameters to the `npm run generate-migration` command, as it will fail. -- Never create tests for entities. -- Always validate and sanitize user input to prevent SQL injection and other attacks. -- Every model should have a factory method for creating instances from raw data, e.g.: +# Models and Entities Instructions + +## Overview +Models represent your application's data structure and business entities. This project uses TypeORM for database interactions with PostgreSQL, following Domain-Driven Design principles for business logic encapsulation. + +**CRITICAL REQUIREMENTS**: +- **ALL entities are tested through REST API integration tests** (not unit tests) +- Use UUID v4 for all primary keys +- Follow factory pattern for entity creation +- Register all repositories in container +- Generate migrations for all schema changes + +## Entity Structure + +### Basic Entity Pattern ```typescript -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"; export interface UserProps { id: string; firstName: string; lastName: string; email: string; + createdAt?: Date; + updatedAt?: Date; } @Entity({ @@ -30,23 +34,440 @@ export interface UserProps { }) export class UserEntity { public static create(data: Partial): UserEntity { - const object = new UserEntity(); - Object.assign(object, data); - return object; + const entity = new UserEntity(); + Object.assign(entity, data); + return entity; } @PrimaryGeneratedColumn("uuid") id: string; - @Column() + @Column({ name: "first_name" }) firstName: string; - @Column() + @Column({ name: "last_name" }) + lastName: string; + + @Column({ unique: true }) + email: string; + + @CreateDateColumn({ name: "created_at" }) + createdAt: Date; + + @UpdateDateColumn({ name: "updated_at" }) + updatedAt: Date; +} +``` + +### Entity Guidelines +- All entities must have `.entity.ts` suffix +- Use `@PrimaryGeneratedColumn("uuid")` for primary keys +- Include `CreatedDateColumn` and `UpdateDateColumn` for audit trails +- Use snake_case for database column names with `name` property +- Define TypeScript interface for entity properties +- Implement static `create()` factory method + +## Advanced Entity Patterns + +### Entity with Business Logic +For entities that need business rules and validation: + +```typescript +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +export interface UserProps { + id: string; + firstName: string; lastName: string; + email: string; + status: UserStatus; +} + +export enum UserStatus { + ACTIVE = 'active', + INACTIVE = 'inactive', + SUSPENDED = 'suspended' +} + +@Entity({ name: "user" }) +export class UserEntity { + public static create(data: Partial): UserEntity { + const entity = new UserEntity(); + Object.assign(entity, data); + + // Business rule: new users are active by default + if (!entity.status) { + entity.status = UserStatus.ACTIVE; + } + + // Validate business rules + entity.validateBusinessRules(); + + return entity; + } + + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ name: "first_name" }) + firstName: string; + + @Column({ name: "last_name" }) + lastName: string; + + @Column({ unique: true }) + email: string; + + @Column({ + type: "enum", + enum: UserStatus, + default: UserStatus.ACTIVE + }) + status: UserStatus; + + // Business methods + public getFullName(): string { + return `${this.firstName} ${this.lastName}`; + } + + public isActive(): boolean { + return this.status === UserStatus.ACTIVE; + } + + public suspend(): void { + if (this.status === UserStatus.SUSPENDED) { + throw new Error("User is already suspended"); + } + this.status = UserStatus.SUSPENDED; + } + + public activate(): void { + this.status = UserStatus.ACTIVE; + } + + // Private validation + private validateBusinessRules(): void { + if (!this.email || !this.email.includes('@')) { + throw new Error("Valid email is required"); + } + + if (!this.firstName?.trim() || !this.lastName?.trim()) { + throw new Error("First name and last name are required"); + } + } +} +``` + +### Entity Relationships +```typescript +import { Entity, Column, OneToMany, ManyToOne, JoinColumn } from "typeorm"; + +@Entity({ name: "order" }) +export class OrderEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ name: "user_id" }) + userId: string; + + @ManyToOne(() => UserEntity) + @JoinColumn({ name: "user_id" }) + user: UserEntity; + + @OneToMany(() => OrderItemEntity, orderItem => orderItem.order) + items: OrderItemEntity[]; + + @Column({ name: "total_amount", type: "decimal", precision: 10, scale: 2 }) + totalAmount: number; +} + +@Entity({ name: "order_item" }) +export class OrderItemEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ name: "order_id" }) + orderId: string; + + @ManyToOne(() => OrderEntity, order => order.items) + @JoinColumn({ name: "order_id" }) + order: OrderEntity; @Column() + quantity: number; + +## Repository Registration + +### Container Registration +Always register repositories in `src/container/database.ts`: + +```typescript +import { AwilixContainer, asValue } from "awilix"; +import { dataSource } from "../config/db"; +import { UserEntity } from "../app/features/users/models/user.entity"; +import { OrderEntity } from "../app/features/orders/models/order.entity"; + +export async function registerDatabase(container: AwilixContainer) { + await dataSource.initialize(); + + // Run migrations automatically + try { + await dataSource.runMigrations(); + } catch (err) { + container.cradle.logger.debug(`Migrations error: ${err}`); + throw err; + } + + // Register repositories + container.register({ + dbDataSource: asValue(dataSource), + + // Entity repositories + userRepository: asValue(dataSource.getRepository(UserEntity)), + orderRepository: asValue(dataSource.getRepository(OrderEntity)), + // Add new repositories here + }); +} +``` + +### Repository Usage Pattern +Use repositories in handlers for data access: + +```typescript +export interface CreateUserHandlerDependencies { + userRepository: Repository; + logger: Logger; +} + +export default class CreateUserHandler implements CommandHandler { + constructor(private dependencies: CreateUserHandlerDependencies) {} + + async execute(command: CreateUserCommand) { + // Use entity factory method + const userEntity = UserEntity.create({ + firstName: command.payload.firstName, + lastName: command.payload.lastName, + email: command.payload.email, + }); + + // Save using repository + const savedUser = await this.dependencies.userRepository.save(userEntity); + + return { result: savedUser }; + } +} +``` + +## Repository Operations + +### Query Optimization +Optimize database queries in query handlers: + +```typescript + // Select only needed fields + await this.userRepository + .createQueryBuilder('user') + .select(['user.id', 'user.name', 'user.email']) + .leftJoinAndSelect('user.profile', 'profile') + .where('user.id = :id', { id }) + .getOne(); +} +``` + +### Advanced Repository Patterns + +#### Bulk Operations +```typescript +// Bulk insert +await this.userRepository + .createQueryBuilder() + .insert() + .into(UserEntity) + .values(userArray) + .execute(); + +// Bulk update +await this.userRepository + .createQueryBuilder() + .update(UserEntity) + .set({ status: UserStatus.INACTIVE }) + .where("lastLoginAt < :date", { date: inactiveDate }) + .execute(); +``` + +#### Complex Queries +```typescript +// Query with multiple conditions and joins +const users = await this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.orders', 'order') + .leftJoinAndSelect('order.items', 'item') + .where('user.status = :status', { status: UserStatus.ACTIVE }) + .andWhere('order.createdAt > :date', { date: lastMonth }) + .orderBy('user.createdAt', 'DESC') + .take(10) + .getMany(); +``` + +#### Transactions +```typescript +// Using transactions for multi-table operations +await this.userRepository.manager.transaction(async manager => { + const user = await manager.save(UserEntity, userData); + const profile = await manager.save(ProfileEntity, { ...profileData, userId: user.id }); + await manager.save(AuditEntity, { action: 'USER_CREATED', userId: user.id }); +}); +``` + +## Database Migrations + +### Migration Generation +Always generate migrations using the npm script: + +```bash +# Generate migration after entity changes +npm run generate-migration + +# This will auto-generate migration name based on changes +# Never add parameters to this command +``` + +### Migration Best Practices +- **Always generate migrations** after entity changes +- **Never create migrations manually** - use the auto-generation +- **Never add parameters** to `npm run generate-migration` +- Migrations are **auto-executed** at application start +- Review generated migrations before committing + +### Example Migration Structure +```typescript +// Auto-generated migration example +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddUserEntity1710240086737 implements MigrationInterface { + name = "AddUserEntity1710240086737"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "user" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "first_name" character varying NOT NULL, + "last_name" character varying NOT NULL, + "email" character varying NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "UQ_user_email" UNIQUE ("email"), + CONSTRAINT "PK_user_id" PRIMARY KEY ("id") + )` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user"`); + } +} +``` + +## Data Security + +### SQL Injection Prevention +- **Use repository methods** instead of raw queries +- **Leverage TypeORM's query builder** for complex queries +- **Validate all input** in entity factory methods +- **Use parameterized queries** when raw SQL is necessary + +```typescript +// Safe: Using repository methods +const users = await userRepository.findBy({ status: UserStatus.ACTIVE }); + +// Safe: Using query builder +const users = await userRepository + .createQueryBuilder("user") + .where("user.status = :status", { status: UserStatus.ACTIVE }) + .getMany(); +``` + +## Testing Strategy + +### Integration Testing +Entities are tested through REST API integration tests: + +```typescript +describe("POST /api/users", () => { + it("should create user with valid data", async () => { + // This tests UserEntity.create() indirectly + const userData = { + firstName: "John", + lastName: "Doe", + email: "john.doe@example.com" + }; + + const response = await request(app) + .post("/api/users") + .send(userData) + .expect(201); + + expect(response.body.id).to.be.a('string'); + expect(response.body.email).to.equal(userData.email.toLowerCase()); + }); + + it("should reject invalid email", async () => { + const userData = { + firstName: "John", + lastName: "Doe", + email: "invalid-email" + }; + + await request(app) + .post("/api/users") + .send(userData) + .expect(400); + }); +}); +``` + +### Why No Unit Tests for Entities +- Business logic is tested through realistic API scenarios +- Database interactions are tested with actual database +- Entity validation is verified through integration tests +- Factory methods are tested indirectly through handlers + +## Best Practices + +### Entity Design +- **Single Responsibility**: Each entity represents one business concept +- **Encapsulation**: Use private methods for internal business rules +- **Immutability**: Prefer creating new instances over modifying existing ones +- **Validation**: Validate business rules in factory methods and business methods +- **Naming**: Use descriptive names that reflect business domain + +### Database Design +- **Use UUIDs**: Always use UUID v4 for primary keys +- **Audit Trails**: Include `created_at` and `updated_at` timestamps +- **Constraints**: Use database constraints for data integrity +- **Indexes**: Add indexes for frequently queried columns +- **Snake Case**: Use snake_case for database column names + +### Performance Considerations +- **Lazy Loading**: Use lazy loading for related entities when appropriate +- **Query Optimization**: Use `select` to limit returned columns +- **Batch Operations**: Use batch operations for bulk data changes +- **Connection Pooling**: Leverage TypeORM's connection pooling + +### Command Integration +Use entity props interfaces in commands for type safety: + +```typescript +// Command using entity props interface +export interface CreateUserCommandPayload extends Partial { + firstName: string; + lastName: string; email: string; } + +export class CreateUserCommand implements Command { + constructor(public payload: CreateUserCommandPayload) {} +} ``` -- Use factory methods for creating instances from raw data. -- For CRUD method you can use models props interface in commands as a type \ No newline at end of file + +This ensures consistency between your commands and entities while maintaining type safety throughout the application. diff --git a/.github/instructions/quality.instructions.md b/.github/instructions/quality.instructions.md index 4013a30..f9c0672 100644 --- a/.github/instructions/quality.instructions.md +++ b/.github/instructions/quality.instructions.md @@ -1,14 +1,95 @@ --- -applyTo: "**" +applyTo: "**/*.{ts,js}" --- -- Run `npm run lint` to check for linting errors after making changes. -- Run `npm run format` to format the code according to the project's style guide. -- Run `npm run integration` to execute integration tests after making changes. -- Run `npm run units` to execute unit tests after making changes. -- Always ensure that your code passes all tests before finishing. -- Always ensure that code is well-documented, especially public methods and classes. -- Always ensure that your code is formated according to the project's style guide. -- Application supports `/health` endpoint for health checks, which returns 200 OK status if the application is running correctly. -- All errors are handled by `src/middleware/error-handler.ts`, which returns a structured JSON response with an error message and code. -- In case of changing errors, always update the error handler to handle new error types. +# Code Quality Guidelines + +## Core Quality Commands +- **`npm run lint`** - Check for linting errors using ESLint with Airbnb TypeScript configuration +- **`npm run lint-fix`** - Automatically fix linting errors where possible +- **`npm run format`** - Format code using Prettier (printWidth: 120, trailingComma: all) +- **`npm run integration`** - Execute integration tests using Mocha, Chai, and Supertest +- **`npm run units`** - Execute unit tests for services and utilities +- **`npm run integration-tests-coverage`** - Run integration tests with nyc coverage reporting +- **`npm run services-units-coverage`** - Run unit tests with nyc coverage reporting + +## Pre-commit Quality Gates +- **Husky pre-push hook** automatically runs linting and unit tests before pushing +- **Commitlint** enforces conventional commit message format +- All quality checks must pass before code can be merged + +## Code Style Standards +- **ESLint Configuration**: Extends Airbnb TypeScript base, Prettier recommended +- **File Naming**: Use kebab-case for all files (enforced by unicorn/filename-case rule) +- **Interface Naming**: PascalCase without "I" prefix (enforced by @typescript-eslint/naming-convention) +- **String Quotes**: Double quotes only (enforced by @typescript-eslint/quotes) +- **Console Usage**: No console.log statements allowed in production code (use Logger instead) +- **Test Files**: Use .spec.ts suffix, excluded from unused-expressions rule + +## TypeScript Quality +- **Strict Mode**: All TypeScript strict checks enabled +- **No Unused Variables**: Error on unused variables (argsIgnorePattern: "^_" for intentionally unused) +- **Decorator Support**: Experimental decorators enabled for dependency injection +- **Source Maps**: Inline source maps for better debugging + +## Testing Quality Requirements +- **Coverage Reporting**: nyc configured for both unit and integration test coverage +- **Test Environment**: Mocha with TypeScript support, Chai assertions, Supertest for API testing +- **Test Structure**: Separate unit tests for services, integration tests for endpoints +- **No Test-Only**: Prevent .only() in test files (no-only-tests plugin) + +## Documentation Standards +- **TSDoc Comments**: Use JSDoc-style comments for all public methods and classes +- **API Documentation**: Swagger/OpenAPI documentation in swagger/ directory +- **Feature Documentation**: Each feature should have clear purpose and usage documentation +- **Error Documentation**: Document all custom error types and their usage + +## Health Monitoring +- **Health Endpoint**: `/health` endpoint returns 200 OK when application is healthy +- **Database Health**: Health checks should validate database connectivity +- **Service Dependencies**: Monitor external service availability in health checks + +## Performance Standards +- **Build Performance**: Use SWC for faster TypeScript compilation in development +- **Bundle Analysis**: Regular analysis of build output size and dependencies +- **Memory Usage**: Monitor memory consumption in long-running processes +- **Response Times**: API endpoints should respond within acceptable time limits + +## Security Quality +- **Dependency Scanning**: Regular security audits of npm packages +- **Input Validation**: All inputs validated using Celebrate/Joi schemas +- **Security Headers**: Helmet middleware for security headers +- **CORS Configuration**: Proper CORS setup for allowed origins + +## Git Quality Standards +- **Conventional Commits**: Follow conventional commit format (enforced by commitlint) +- **Branch Naming**: Use descriptive branch names with feature/ or fix/ prefixes +- **Pull Request Quality**: Comprehensive PR descriptions with testing notes +- **Code Review**: All changes require code review before merging + +## CI/CD Quality +- **Docker Containerization**: All services containerized for consistent environments +- **Pipeline Validation**: Automated quality checks in CI/CD pipelines +- **Environment Parity**: Development, staging, and production environment consistency +- **Deployment Verification**: Post-deployment health checks and smoke tests + +## Quality Metrics +- **Code Coverage**: Maintain minimum coverage thresholds for unit and integration tests +- **Cyclomatic Complexity**: Monitor and reduce complex functions +- **Technical Debt**: Regular refactoring to address code smells and technical debt +- **Performance Metrics**: Track and optimize key performance indicators + +## Development Workflow +1. **Before Coding**: Ensure local environment is up to date +2. **During Development**: Run tests frequently, use watch mode for immediate feedback +3. **Before Committing**: Run full quality suite (lint, format, tests) +4. **Before Pushing**: Verify all quality gates pass +5. **After Merging**: Monitor deployment and quality metrics + +## Quality Tools Integration +- **SonarQube**: Static code analysis and quality metrics +- **nyc**: Test coverage reporting with LCOV format +- **Prettier**: Consistent code formatting +- **ESLint**: Code quality and style enforcement +- **TypeScript Compiler**: Type checking and compilation +- **Husky**: Git hooks for quality enforcement diff --git a/.github/instructions/routing.instructions.md b/.github/instructions/routing.instructions.md index 9257f68..ae8316c 100644 --- a/.github/instructions/routing.instructions.md +++ b/.github/instructions/routing.instructions.md @@ -1,52 +1,373 @@ --- -applyTo: "**/features/**/routing.ts" +applyTo: "**/routing.ts" --- -- Routing is divided into three main areas: - - Feature routing: `src/app/features/{feature}/routing.ts` - - Global routing: `src/app/router.ts` - - Application routing: `src/app/index.ts` -- Use feature routing for individual actions registering and middlewares related to specific action only. -- Use global routing for feature-wide actions, commands, and queries and middlewares that apply to all actions in the feature. -- Use application routing for application-wide actions, commands, and queries and middlewares that apply to all features. -- All application routes starts with `/api/` -- Register feature routing in `src/app/router.ts` -- When registering routing in `src/app/router.ts`, always register it with specific path, for example: -```typescript -router.use("/example", usersRouting); -``` -- For list endpoints, use `GET` method and `/` format for path -- For single item endpoints, use `GET` method and `/:id` format for path -- For creating items, use `POST` method and `/` format for path -- For updating items, use `PUT` method and `/:id` format for path -- For deleting items, use `DELETE` method and `/:id` format for path -- A good example of routing: -```typescript -export const exampleRouting = (actions: ExampleRoutingDependencies) => { +# Routing Instructions + +## Overview +Routing in this application follows a hierarchical structure that separates concerns and enables modular development. The routing system consists of three main layers: +1. **Feature Routing** (`src/app/features/{feature}/routing.ts`) - Individual feature endpoints +2. **Global Routing** (`src/app/router.ts`) - Feature registration and global routes +3. **Application Routing** (`src/app/app.ts`) - Application-wide middleware and configuration + +**CRITICAL REQUIREMENTS**: +- All routes are automatically tested through REST API integration tests +- Follow RESTful conventions for HTTP methods and paths +- Use middleware for validation, authentication, and request processing +- Maintain consistent route structure across features + +## Routing Hierarchy + +### Feature Routing Pattern +Each feature has its own routing file that defines endpoints and their middleware: + +```typescript +import express from "express"; +import { Action } from "../../../shared/http/types"; + +// Import validation middleware +import { createUserActionValidation } from "./actions/create-user.action"; +import { getUserActionValidation } from "./actions/get-user.action"; +import { getUsersActionValidation } from "./actions/get-users.action"; +import { updateUserActionValidation } from "./actions/update-user.action"; +import { deleteUserActionValidation } from "./actions/delete-user.action"; + +export interface UsersRoutingDependencies { + createUserAction: Action; + getUserAction: Action; + getUsersAction: Action; + updateUserAction: Action; + deleteUserAction: Action; +} + +export const usersRouting = (actions: UsersRoutingDependencies) => { const router = express.Router(); - router.post( + // List endpoint - GET / + router.get( "/", - [createExampleActionValidation], - actions.createExampleAction.invoke.bind(actions.createExampleAction), + [getUsersActionValidation], + actions.getUsersAction.invoke.bind(actions.getUsersAction) ); - router.get( + + // Create endpoint - POST / + router.post( "/", - [examplesActionValidation], - actions.examplesAction.invoke.bind(actions.examplesAction), + [createUserActionValidation], + actions.createUserAction.invoke.bind(actions.createUserAction) ); + + // Single item endpoint - GET /:id router.get( "/:id", - [exampleActionValidation], - actions.exampleAction.invoke.bind(actions.exampleAction), + [getUserActionValidation], + actions.getUserAction.invoke.bind(actions.getUserAction) ); + + // Update endpoint - PUT /:id + router.put( + "/:id", + [updateUserActionValidation], + actions.updateUserAction.invoke.bind(actions.updateUserAction) + ); + + // Delete endpoint - DELETE /:id router.delete( "/:id", - [deleteExampleActionValidation], - actions.deleteExampleAction.invoke.bind(actions.deleteExampleAction), + [deleteUserActionValidation], + actions.deleteUserAction.invoke.bind(actions.deleteUserAction) ); - // ACTIONS_SETUP return router; }; ``` + +### Global Routing Registration +Register feature routes in `src/app/router.ts`: + +```typescript +import express from "express"; + +export interface RoutingDependencies { + usersRouting: express.Router; + ordersRouting: express.Router; + productsRouting: express.Router; +} + +export const createRouter = ({ + usersRouting, + ordersRouting, + productsRouting, +}: RoutingDependencies) => { + const router = express.Router(); + + // Register feature routes with descriptive paths + router.use("/users", usersRouting); + router.use("/orders", ordersRouting); + router.use("/products", productsRouting); + + return router; +}; +``` + +### Container Registration +Register routing dependencies in `src/container/routing.ts`: + +```typescript +import { AwilixContainer, Lifetime, asClass, asFunction } from "awilix"; +import { usersRouting } from "../app/features/users/routing"; +import { ordersRouting } from "../app/features/orders/routing"; + +export async function registerRouting(container: AwilixContainer) { + // Auto-load all action classes + container.loadModules(["src/app/**/*.action.js"], { + formatName: "camelCase", + resolverOptions: { + lifetime: Lifetime.SCOPED, + register: asClass, + }, + }); + + // Register routing functions + container.register({ + usersRouting: asFunction(usersRouting), + ordersRouting: asFunction(ordersRouting), + }); + +## RESTful Route Conventions + +### Standard HTTP Methods and Paths +Follow these conventions for consistent API design: + +| Operation | Method | Path | Example | Purpose | +|-----------|--------|------|---------|---------| +| List | `GET` | `/` | `GET /users` | Retrieve paginated list | +| Create | `POST` | `/` | `POST /users` | Create new resource | +| Read | `GET` | `/:id` | `GET /users/123` | Retrieve single resource | +| Update | `PUT` | `/:id` | `PUT /users/123` | Update entire resource | +| Partial Update | `PATCH` | `/:id` | `PATCH /users/123` | Update specific fields | +| Delete | `DELETE` | `/:id` | `DELETE /users/123` | Remove resource | + +### Route Order Importance +Order routes from most specific to least specific: + +```typescript +export const usersRouting = (actions: UsersRoutingDependencies) => { + const router = express.Router(); + + // Specific routes first + router.post("/bulk", actions.bulkCreateUsersAction.invoke.bind(actions.bulkCreateUsersAction)); + router.get("/search", actions.searchUsersAction.invoke.bind(actions.searchUsersAction)); + + // Standard CRUD routes + router.get("/", actions.getUsersAction.invoke.bind(actions.getUsersAction)); + router.post("/", actions.createUserAction.invoke.bind(actions.createUserAction)); + + // Parameterized routes last + router.get("/:id", actions.getUserAction.invoke.bind(actions.getUserAction)); + router.put("/:id", actions.updateUserAction.invoke.bind(actions.updateUserAction)); + router.delete("/:id", actions.deleteUserAction.invoke.bind(actions.deleteUserAction)); + + return router; +}; +``` + +## Middleware Integration + +### Validation Middleware +Always include validation middleware for each endpoint: + +```typescript +// Single validation middleware +router.post( + "/", + [createUserActionValidation], + actions.createUserAction.invoke.bind(actions.createUserAction) +); + +// Multiple middleware chain +router.put( + "/:id", + [ + authenticationMiddleware, // Check if user is logged in + updateUserActionValidation, // Validate request data + authorizationMiddleware, // Check if user can update + ], + actions.updateUserAction.invoke.bind(actions.updateUserAction) +); +``` + +### Authentication and Authorization +Add security middleware for protected endpoints: + +```typescript +import { authenticateToken } from "../../../middleware/auth.middleware"; +import { authorizeRole } from "../../../middleware/role.middleware"; + +export const usersRouting = (actions: UsersRoutingDependencies) => { + const router = express.Router(); + + // Public endpoints (no auth required) + router.post("/register", [registerUserActionValidation], actions.registerUserAction.invoke.bind(actions.registerUserAction)); + router.post("/login", [loginActionValidation], actions.loginAction.invoke.bind(actions.loginAction)); + + // Protected endpoints (auth required) + router.get( + "/", + [authenticateToken, getUsersActionValidation], + actions.getUsersAction.invoke.bind(actions.getUsersAction) + ); + + // Admin-only endpoints + router.delete( + "/:id", + [ + authenticateToken, + authorizeRole(['admin']), + deleteUserActionValidation + ], + actions.deleteUserAction.invoke.bind(actions.deleteUserAction) + ); + + return router; +}; +``` + +## Advanced Routing Patterns + +### Nested Resources +Handle nested resource relationships: + +```typescript +// For routes like /users/:userId/orders +export const userOrdersRouting = (actions: UserOrdersRoutingDependencies) => { + const router = express.Router({ mergeParams: true }); // Important for nested params + + router.get("/", [getUserOrdersActionValidation], actions.getUserOrdersAction.invoke.bind(actions.getUserOrdersAction)); + router.post("/", [createUserOrderActionValidation], actions.createUserOrderAction.invoke.bind(actions.createUserOrderAction)); + router.get("/:orderId", [getUserOrderActionValidation], actions.getUserOrderAction.invoke.bind(actions.getUserOrderAction)); + + return router; +}; + +// In main router +router.use("/users/:userId/orders", userOrdersRouting); +``` + +### Versioned APIs +Support API versioning: + +```typescript +// src/app/router.ts +export const createRouter = ({ v1Router, v2Router }: RoutingDependencies) => { + const router = express.Router(); + + router.use("/v1", v1Router); + router.use("/v2", v2Router); + + // Default to latest version + router.use("/", v2Router); + + return router; +}; +``` + +### Query Parameter Routing +Handle complex query parameters: + +```typescript +// Routes with different behavior based on query params +router.get( + "/", + [getUsersActionValidation], + (req, res, next) => { + // Route to different actions based on query params + if (req.query.export === 'csv') { + return actions.exportUsersAction.invoke(req, res, next); + } + return actions.getUsersAction.invoke(req, res, next); + } +); +``` + +## Application Integration + +### Main Application Setup +Routes are integrated in `src/app/app.ts`: + +```typescript +// All feature routes are prefixed with /api +app.use("/api", router); + +// Results in URLs like: +// POST /api/users +// GET /api/users/123 +// PUT /api/orders/456 +``` + +### Health Check and Documentation +Standard endpoints are configured at application level: + +```typescript +// Health check endpoint +app.get("/health", (req, res) => { + res.status(200).json({ status: "ok" }); +}); + +// API documentation +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +// GraphQL endpoint +app.use("/graphql", graphQLMiddleware); +``` + +## Best Practices + +### Route Organization +- **Feature-based**: Group routes by business feature, not technical concern +- **Consistent naming**: Use plural nouns for resources (`/users`, not `/user`) +- **Hierarchical**: Organize nested resources logically +- **Descriptive paths**: Use clear, descriptive route paths + +### Middleware Best Practices +- **Order matters**: Place authentication before authorization +- **Validation first**: Validate input before processing +- **Reusable middleware**: Create reusable middleware functions + +### Security Considerations +- **Authentication**: Protect sensitive endpoints +- **Authorization**: Check user permissions +- **Rate limiting**: Add rate limiting for public endpoints +- **Input validation**: Always validate incoming data + +### Performance Optimization +- **Route caching**: Cache responses for expensive operations +- **Compression**: Use compression middleware +- **Static files**: Serve static files efficiently +- **Connection pooling**: Optimize database connections + +### Testing Integration +Routes are tested through integration tests: + +```typescript +describe("Users API", () => { + it("should create user via POST /api/users", async () => { + await request(app) + .post("/api/users") + .send({ name: "John Doe", email: "john@example.com" }) + .expect(201); + }); + + it("should get user via GET /api/users/:id", async () => { + const user = await createTestUser(); + await request(app) + .get(`/api/users/${user.id}`) + .expect(200) + .expect(res => { + expect(res.body.id).to.equal(user.id); + }); + }); +}); +``` + +This routing structure ensures scalable, maintainable, and testable API endpoints while following REST conventions and security best practices. diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md index 90feb2a..e637fd6 100644 --- a/.github/instructions/security.instructions.md +++ b/.github/instructions/security.instructions.md @@ -1,20 +1,310 @@ --- -applyTo: "**" +applyTo: "**/*.{ts,js}" --- -- When implmenting authentication, use JWT tokens for stateless authentication. -- Never store sensitive information (like passwords) in plain text, always hash them using a strong hashing algorithm (e.g. SHA-256). -- Always implement authentication with proper session management, including token expiration and refresh mechanisms. -- The authentication middleware should validate the JWT token and extract user information from it. -- Use role-based access control (RBAC) to manage permissions and restrict access to resources based on user roles. -- Always validate user input to prevent common security vulnerabilities such as SQL injection, XSS, and CSRF. -- Authentication endpoints should return both access and refresh tokens upon successful login: +# Security Guidelines for Express Boilerplate + +## Core Security Libraries +- **Helmet**: Security headers middleware (already configured in `src/app/app.ts`) +- **CORS**: Cross-origin resource sharing control (configured for GraphQL and REST) +- **Celebrate/Joi**: Input validation and sanitization +- **TypeORM**: Built-in SQL injection protection with parameterized queries +- **Body-parser**: Request body parsing with size limits + +## Authentication & Authorization + +### JWT Implementation +- Use JWT tokens for stateless authentication across REST and GraphQL endpoints +- Store JWT secret in environment variables (never in code) +- Implement token expiration (access) and refresh token mechanisms +- Extract user information from validated JWT tokens in middleware + +### Authentication Patterns +```typescript +// Example authentication middleware +export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new HttpError('Unauthorized', StatusCodes.UNAUTHORIZED); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch (error) { + throw new HttpError('Invalid token', StatusCodes.UNAUTHORIZED); + } +}; +``` + +### Authorization Patterns +- Implement role-based access control (RBAC) using user roles from JWT payload +- Create authorization middleware for different permission levels +- Use route-level authorization guards in routing files + +### Authentication Endpoints +Authentication endpoints must return structured token responses: ```json { - "access_token": "your_access_token", - "refresh_token": "your_refresh_token" + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_in": 3600, + "token_type": "Bearer" } ``` -- Always implement `me` endpoint to allow users to retrieve their own profile information. -- Use `helmet` for security headers in Express. -- Use `cors` middleware to control cross-origin requests, allowing only trusted origins. \ No newline at end of file + +### User Profile Endpoints +- Always implement `/me` endpoint for authenticated user profile retrieval +- Return sanitized user data (exclude passwords, internal IDs) +- Support profile updates with proper validation + +## Input Validation & Sanitization + +### Celebrate/Joi Validation +All endpoints must use Celebrate validation middleware: +```typescript +export const createUserValidation = celebrate({ + body: Joi.object().keys({ + email: Joi.string().email().required(), + password: Joi.string().min(8).required(), + firstName: Joi.string().trim().min(1).max(50).required(), + lastName: Joi.string().trim().min(1).max(50).required(), + }), +}); +``` + +### Validation Security Rules +- Sanitize all string inputs using `Joi.string().trim()` +- Set maximum lengths for all string fields to prevent DoS +- Use strict type validation (no implicit type conversion) +- Validate nested objects and arrays with specific schemas +- Implement custom validation for business rules + +### File Upload Security +- Validate file types using MIME type checking +- Implement file size limits +- Scan uploaded files for malware +- Store files outside the web root +- Use secure file naming conventions + +## Password Security + +### Password Hashing +- **Never store passwords in plain text** +- Use bcrypt with minimum 12 rounds for password hashing +- Implement password strength requirements (minimum 8 characters, complexity) +- Use secure password reset mechanisms with time-limited tokens + +### Example Password Handling +```typescript +import bcrypt from 'bcrypt'; + +// Hashing password +const hashPassword = async (password: string): Promise => { + const saltRounds = 12; + return bcrypt.hash(password, saltRounds); +}; + +// Verifying password +const verifyPassword = async (password: string, hash: string): Promise => { + return bcrypt.compare(password, hash); +}; +``` + +## Security Headers Configuration + +### Helmet Configuration +Helmet is configured in `src/app/app.ts` with Content Security Policy: +```typescript +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + scriptSrc: ["'self'", "https: 'unsafe-inline'"], + }, + }, + }), +); +``` + +### Additional Security Headers +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` +- `X-XSS-Protection: 1; mode=block` +- `Strict-Transport-Security` (for HTTPS) +- `Referrer-Policy: no-referrer` + +## CORS Configuration + +### CORS Setup +Configure CORS for both REST API and GraphQL endpoints: +```typescript +// Basic CORS for REST API +app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], + credentials: true, + optionsSuccessStatus: 200, +})); + +// GraphQL CORS +app.use('/graphql', cors()); +``` + +### CORS Best Practices +- Define explicit allowed origins (avoid wildcard `*` in production) +- Enable credentials only when necessary +- Configure allowed methods and headers specifically +- Use environment variables for origin configuration + +## Environment Variables Security + +### Sensitive Data Management +- Store all secrets in environment variables, never in code +- Use `.env.dist` as template, never commit actual `.env` files +- Validate all environment variables using Joi schemas in config files +- Use different environments for development, staging, and production + +### Required Security Environment Variables +```bash +# Authentication +JWT_SECRET=your_jwt_secret_key_here +JWT_EXPIRES_IN=3600 + +# API Security +AUTH_API_KEY=your_api_key_here +ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com + +# Database (use separate credentials for each environment) +RDS_USERNAME=your_db_user +RDS_PASSWORD=your_secure_db_password +``` + +## Database Security + +### TypeORM Security +- Use parameterized queries (TypeORM default behavior) +- Implement proper indexing for performance and security +- Use database connection pooling with limits +- Enable query logging only in development environments + +### Entity Security +```typescript +@Entity('users') +export class User { + @PrimaryColumn('uuid') + id: string; + + @Column({ unique: true }) + email: string; + + @Column({ select: false }) // Exclude from default selections + password: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} +``` + +## API Security + +### Rate Limiting +- Implement rate limiting for all public endpoints +- Use different limits for authenticated vs. anonymous users +- Implement progressive delays for failed authentication attempts + +### API Key Management +- Use API keys for service-to-service communication +- Rotate API keys regularly +- Implement API key scoping for different access levels + +### Request Security +- Implement request size limits using body-parser configuration +- Validate Content-Type headers +- Log and monitor suspicious request patterns + +## Container Security + +### Docker Security +The production Dockerfile implements security best practices: +- Uses non-root user (`USER node`) +- Sets proper file ownership (`--chown=node:node`) +- Uses Alpine Linux base images for smaller attack surface +- Implements multi-stage builds to reduce final image size + +### Container Best Practices +- Run applications as non-privileged user +- Use specific image tags, not `latest` +- Regularly update base images +- Scan images for vulnerabilities +- Use read-only file systems when possible + +## Logging & Monitoring + +### Security Logging +- Log all authentication attempts (success and failure) +- Log authorization failures +- Log input validation failures +- Monitor for unusual patterns or potential attacks +- Use structured logging format for easier analysis + +### Log Security +- Never log sensitive information (passwords, tokens, personal data) +- Use log rotation and retention policies +- Secure log storage and access +- Implement log integrity checks + +## Security Testing + +### Security Test Requirements +- Test authentication and authorization flows +- Validate input sanitization and validation +- Test error handling for security scenarios +- Implement penetration testing for critical endpoints + +### Example Security Tests +```typescript +describe('Authentication Security', () => { + it('should reject invalid JWT tokens', async () => { + const response = await request(app) + .get('/api/protected') + .set('Authorization', 'Bearer invalid_token') + .expect(401); + }); + + it('should prevent SQL injection in user input', async () => { + const maliciousInput = "'; DROP TABLE users; --"; + const response = await request(app) + .post('/api/users') + .send({ name: maliciousInput }) + .expect(400); + }); +}); +``` + +## Security Checklist + +### Pre-deployment Security Checks +- [ ] All secrets stored in environment variables +- [ ] Input validation implemented for all endpoints +- [ ] Authentication and authorization working correctly +- [ ] Security headers configured properly +- [ ] CORS configured for production origins +- [ ] Database queries use parameterized statements +- [ ] Passwords are properly hashed +- [ ] API rate limiting implemented +- [ ] Security tests passing +- [ ] Container runs as non-root user +- [ ] Dependencies scanned for vulnerabilities + +### Regular Security Maintenance +- [ ] Update dependencies regularly +- [ ] Rotate secrets and API keys +- [ ] Review and update CORS origins +- [ ] Monitor security logs +- [ ] Conduct security audits +- [ ] Update security headers as needed +- [ ] Test backup and recovery procedures \ No newline at end of file diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md index 21e757e..0c15401 100644 --- a/.github/instructions/tests.instructions.md +++ b/.github/instructions/tests.instructions.md @@ -1,28 +1,272 @@ --- -applyTo: "**/tests/*.spec.ts" +applyTo: "**/*.spec.ts" --- -- All tests must have a `.spec.ts` suffix. -- Unit tests should be placed next to the file they test -- Integration tests must be placed in `src/tests` directory -- Tests for each feature must be placed in a separate file, named after the feature (e.g., `users.spec.ts`, `warehouses.spec.ts`). -- Tests for each feature must be placed in a separate directory, named after the feature (e.g., `src/tests/users`, `src/tests/warehouses`). -- Use following pattern for integration tests: +## Test Structure and Organization + +### File Naming and Location +- All test files must have a `.spec.ts` suffix +- **Unit tests**: Place next to the file they test (e.g., `src/shared/pagination-utils/pagination-utils.spec.ts`) +- **Integration tests**: Place in `src/tests` directory organized by feature (e.g., `src/tests/users/users.spec.ts`) +- Each feature should have its own test file and directory structure +- Shared test utilities should be placed in `src/tests/shared/` + +### Test Categories and Commands +- **Unit tests**: `npm run units` - Tests individual functions, utilities, and business logic +- **Integration tests**: `npm run integration` - Tests complete API endpoints and feature workflows +- **Coverage reports**: Use `npm run services-units-coverage` and `npm run integration-tests-coverage` + +## Integration Test Pattern + +### Basic Structure ```typescript import { expect } from "chai"; import "mocha"; import request from "supertest"; -describe("/api/{{kebabCase name}} integration", () => { - it("test example", async () => { - return request(await global.container.cradle.app) - .get("/health") - .expect(200) +describe("Feature Name API", () => { + describe("POST /api/feature", () => { + it("should create a new resource successfully", async () => { + const payload = { + name: "Test Resource", + description: "Test Description" + }; + + const response = await request(global.container.cradle.app) + .post("/api/feature") + .send(payload) + .expect(201); + + expect(response.body).to.have.property("id"); + expect(response.body.name).to.equal(payload.name); + }); + + it("should return validation error for invalid input", async () => { + const invalidPayload = { name: "" }; + + await request(global.container.cradle.app) + .post("/api/feature") + .send(invalidPayload) + .expect(400); + }); + }); + + describe("GET /api/feature", () => { + it("should return paginated list", async () => { + const response = await request(global.container.cradle.app) + .get("/api/feature?page=1&limit=10") + .expect(200); + + expect(response.body).to.have.property("data"); + expect(response.body).to.have.property("meta"); + expect(response.body.meta).to.have.property("pagination"); + }); + }); +}); +``` + +### Advanced Integration Test Patterns +```typescript +describe("Feature with Authentication", () => { + let authToken: string; + let testUserId: string; + + beforeEach(async () => { + // Create test user and get auth token + const authResponse = await request(global.container.cradle.app) + .post("/api/auth/login") + .send({ email: "test@example.com", password: "password" }) + .expect(200); + + authToken = authResponse.body.access_token; + testUserId = authResponse.body.user.id; + }); + + it("should require authentication", async () => { + await request(global.container.cradle.app) + .get("/api/protected-resource") + .expect(401); + }); + + it("should access protected resource with valid token", async () => { + const response = await request(global.container.cradle.app) + .get("/api/protected-resource") + .set("Authorization", `Bearer ${authToken}`) + .expect(200); + + expect(response.body).to.have.property("data"); }); }); ``` -- Test bootstrap is in `src/tests/bootstrap.ts`, which sets up the container, database connection, and clears the database before each test. -- Use `clearDb()` function in `src/tests/bootstrap.ts` to clear the database before each test. -- Every repository must be added to `clearDb()` function and cleared with `.delete({})` to avoid TypeORM issues. -- Use `global.container` and `global.dbConnection` in tests for accessing the container and database connection. -- The tests use separate `.env.test` file for environment variables. All environment variables has to be added there too. \ No newline at end of file + +## Unit Test Pattern + +### Utility Function Testing +```typescript +import "mocha"; +import { expect } from "chai"; +import { functionToTest } from "./utility-file"; + +describe("Utility Function Name", () => { + it("should handle valid input correctly", () => { + const result = functionToTest("valid input"); + expect(result).to.equal("expected output"); + }); + + it("should throw error for invalid input", () => { + expect(() => functionToTest(null)).to.throw("Expected error message"); + }); + + it("should handle edge cases", () => { + const result = functionToTest(""); + expect(result).to.be.null; + }); +}); +``` + +### Handler/Service Testing with Mocks +```typescript +import "mocha"; +import { expect } from "chai"; +import { SinonStub, stub } from "sinon"; +import { YourHandler } from "./your-handler"; + +describe("YourHandler", () => { + let handler: YourHandler; + let repositoryStub: SinonStub; + + beforeEach(() => { + repositoryStub = stub(); + handler = new YourHandler(repositoryStub); + }); + + afterEach(() => { + repositoryStub.restore?.(); + }); + + it("should execute command successfully", async () => { + const command = { id: "test-id", name: "test-name" }; + repositoryStub.findOne.resolves({ id: "test-id" }); + + const result = await handler.execute(command); + + expect(result).to.have.property("id", "test-id"); + expect(repositoryStub.findOne.calledOnce).to.be.true; + }); +}); +``` + +## Test Environment Setup + +### Bootstrap Configuration +- Test bootstrap is in `src/tests/bootstrap.ts` +- Sets up dependency injection container and database connection +- Uses separate `.env.test` file for test environment variables +- Automatically drops and recreates database before test suite +- Global variables available: `global.container`, `global.dbConnection` + +### Database Cleanup +```typescript +// In src/tests/bootstrap.ts - Add repositories to clearDb function +const clearDb = async (dataSource: DataSource) => { + const repositories = [ + dataSource.getRepository(UserEntity), + dataSource.getRepository(ProductEntity), + // Add all your entities here + ]; + + for (const repository of repositories) { + await repository.delete({}); + } +}; +``` + +### Environment Variables +- All test environment variables must be defined in `.env.test` +- Copy all production environment variables and modify for test environment +- Use test-specific database credentials and service URLs +- Ensure test isolation by using separate resources + +## Testing Best Practices + +### Test Coverage Requirements +- **Integration tests**: Mandatory for all API endpoints +- **Unit tests**: Mandatory for all utility functions and business logic +- **Error scenarios**: Test both success and failure paths (see error-handling.instructions.md) +- **Edge cases**: Test boundary conditions and edge cases + +### Test Data Management +```typescript +describe("Feature Tests", () => { + beforeEach(async () => { + // Clear database and seed test data + await global.dbConnection.synchronize(true); + + // Create test data + const repository = global.dbConnection.getRepository(TestEntity); + await repository.save({ name: "Test Data" }); + }); +}); +``` + +### Assertion Patterns +```typescript +// Use specific assertions +expect(response.status).to.equal(201); +expect(response.body).to.have.property("id"); +expect(response.body.name).to.equal("Expected Name"); + +// Test array responses +expect(response.body.data).to.be.an("array"); +expect(response.body.data).to.have.lengthOf.at.least(1); + +// Test pagination metadata +expect(response.body.meta.pagination).to.deep.include({ + page: 1, + limit: 10, + total: expect.any(Number) +}); +``` + +### Authentication Testing +```typescript +// Test protected endpoints +describe("Protected Endpoints", () => { + it("should reject requests without token", async () => { + await request(global.container.cradle.app) + .get("/api/protected") + .expect(401); + }); + + it("should reject requests with invalid token", async () => { + await request(global.container.cradle.app) + .get("/api/protected") + .set("Authorization", "Bearer invalid-token") + .expect(401); + }); +}); +``` + + +## Test Quality Requirements + +### Mandatory Test Scenarios +- **CRUD operations**: Create, Read, Update, Delete for each entity +- **Validation**: Test all input validation (see error-handling.instructions.md) +- **Authentication**: Test protected endpoints with and without valid tokens +- **Authorization**: Test role-based access control where applicable +- **Pagination**: Test list endpoints with pagination parameters +- **Search and filtering**: Test query parameters and search functionality + +### Test Naming Conventions +- Use descriptive test names that explain the expected behavior +- Start with "should" followed by the expected outcome +- Include context about input/conditions when relevant +- Group related tests using nested `describe` blocks + +### Code Quality in Tests +- Keep tests focused and test only one thing per test case +- Use meaningful variable names and clear test data +- Avoid complex logic in tests - tests should be simple and readable +- Use helper functions for common test setup and assertions +- Maintain test code with the same quality standards as production code \ No newline at end of file