docs(specs): add domain services layer design specification

This commit is contained in:
Lucas Winther
2026-09-05 17:37:53 +02:00
parent f5bff8a81f
commit 091791faef
@@ -0,0 +1,210 @@
# Design Specification: Domain Services Layer
- **Date**: 2026-09-05
- **Status**: Draft (Under Review)
- **Target**: `openproject-mcp` Phase 1 (Task 2: Domain Services)
- **Author**: Antigravity Agent & User Pairing
---
## 1. Executive Summary
This specification defines the Domain Services layer for `openproject-mcp`. The Domain Services layer sits directly between the OpenProject REST API v3 client (`src/client/`) and the upcoming MCP tools layer (`src/tools/`).
Domain services provide stateless, strongly-typed TypeScript functions that abstract OpenProject REST endpoints, automate HAL+JSON unpack/normalization, apply LLM-friendly filter translation, and integrate seamlessly with `RequestContext` via `AsyncLocalStorage`.
---
## 2. Architecture & Design Principles
### 2.1. Stateless Functional Modules with Optional Client Override
Each domain service is implemented as a functional module exporting standalone asynchronous functions. Every function accepts an optional `client?: OpenProjectClient` parameter as its final argument:
- **Default Execution**: If `client` is omitted, the service resolves the client from the active execution context via `getRequestContext().client`. This enables MCP tool handlers to call service functions with zero boilerplate.
- **Testing Ergonomics**: If `client` is explicitly provided, the service uses that instance directly. This allows unit tests to test services with mock clients without requiring `AsyncLocalStorage` setup.
### 2.2. Directory & File Organization
All domain service code is placed in `src/services/`:
- `src/services/projects.ts`: Project listing, details, and schema inspection.
- `src/services/work-packages.ts`: Work package browsing, details, and search.
- `src/services/queries.ts`: Saved query listing, query configuration details, and query result execution.
- `src/services/metadata.ts`: System taxonomies including statuses, work package types, priorities, and users.
- `src/services/index.ts`: Unified barrel export for all domain services and related parameter types.
---
## 3. Detailed Component Specifications
### 3.1. Client Resolution Helper
A shared internal utility function `resolveClient(client?: OpenProjectClient): OpenProjectClient` will be used across all service modules:
```typescript
import { getRequestContext } from "../context.ts";
import type { OpenProjectClient } from "../client/api-client.ts";
export function resolveClient(client?: OpenProjectClient): OpenProjectClient {
return client ?? getRequestContext().client;
}
```
### 3.2. Projects Service (`src/services/projects.ts`)
Interacts with `/api/v3/projects`.
#### Types & Interfaces
```typescript
export interface ListProjectsParams {
pageSize?: number;
offset?: number;
sortBy?: string;
filters?: FilterElement[];
}
```
#### Functions
1. `listProjects(params?: ListProjectsParams, client?: OpenProjectClient): Promise<PaginatedResult<ProjectSummary>>`
- Endpoint: `GET /api/v3/projects`
- Query Parameters: `pageSize`, `offset`, `sortBy`, `filters` (JSON stringified).
- Response Normalization: `unpackCollection(response, normalizeProjectSummary)`.
2. `getProject(idOrIdentifier: number | string, client?: OpenProjectClient): Promise<ProjectDetail>`
- Endpoint: `GET /api/v3/projects/{idOrIdentifier}`
- Response Normalization: `normalizeProject(response)`.
3. `getProjectSchema(client?: OpenProjectClient): Promise<Record<string, unknown>>`
- Endpoint: `GET /api/v3/projects/schema`
- Returns the schema definition object for projects.
---
### 3.3. Work Packages Service (`src/services/work-packages.ts`)
Interacts with `/api/v3/work_packages` and project-scoped work package endpoints.
#### Types & Interfaces
```typescript
export interface ListWorkPackagesParams extends WorkPackageFilterParams {
pageSize?: number;
offset?: number;
sortBy?: string;
}
export interface SearchWorkPackagesOptions {
projectId?: number | string;
pageSize?: number;
offset?: number;
}
```
#### Functions
1. `listWorkPackages(params?: ListWorkPackagesParams, client?: OpenProjectClient): Promise<PaginatedResult<WorkPackageSummary>>`
- Builds filter array using `buildWorkPackageFilters(params)`.
- Endpoint: `GET /api/v3/work_packages`
- Query Parameters: `pageSize`, `offset`, `sortBy`, `filters` (JSON stringified).
- Response Normalization: `unpackCollection(response, normalizeWorkPackageSummary)`.
2. `getWorkPackage(id: number, client?: OpenProjectClient): Promise<WorkPackageDetail>`
- Endpoint: `GET /api/v3/work_packages/{id}`
- Response Normalization: `normalizeWorkPackage(response)`.
3. `searchWorkPackages(query: string, options?: SearchWorkPackagesOptions, client?: OpenProjectClient): Promise<PaginatedResult<WorkPackageSummary>>`
- Convenience wrapper around `listWorkPackages` setting `subject: query` and optional `projectId`.
---
### 3.4. Queries Service (`src/services/queries.ts`)
Interacts with `/api/v3/queries`.
#### Types & Interfaces
```typescript
export interface ListQueriesParams {
projectId?: number | string;
pageSize?: number;
offset?: number;
}
export interface QueryResultsParams {
pageSize?: number;
offset?: number;
}
```
#### Functions
1. `listQueries(params?: ListQueriesParams, client?: OpenProjectClient): Promise<PaginatedResult<QuerySummary>>`
- Endpoint: `GET /api/v3/queries`
- If `projectId` is provided, adds project filter: `[{"project":{"operator":"=","values":[String(projectId)]}}]`.
- Query Parameters: `pageSize`, `offset`, `filters`.
- Response Normalization: `unpackCollection(response, normalizeQuerySummary)`.
2. `getQuery(id: number, client?: OpenProjectClient): Promise<QueryDetail>`
- Endpoint: `GET /api/v3/queries/{id}`
- Response Normalization: `normalizeQuery(response)`.
3. `getQueryResults(id: number, params?: QueryResultsParams, client?: OpenProjectClient): Promise<PaginatedResult<WorkPackageSummary>>`
- Endpoint: `GET /api/v3/queries/{id}/results`
- Query Parameters: `pageSize`, `offset`.
- Response Normalization: `unpackCollection(response, normalizeWorkPackageSummary)`.
---
### 3.5. Metadata Service (`src/services/metadata.ts`)
Interacts with `/api/v3/statuses`, `/api/v3/types`, `/api/v3/priorities`, and `/api/v3/users`.
#### Types & Interfaces
```typescript
export interface ListTypesParams {
projectId?: number | string;
}
export interface ListUsersParams {
pageSize?: number;
offset?: number;
status?: string;
}
```
#### Functions
1. `listStatuses(client?: OpenProjectClient): Promise<StatusItem[]>`
- Endpoint: `GET /api/v3/statuses`
- Unpacks collection and extracts elements normalized via `normalizeStatus`.
2. `listTypes(params?: ListTypesParams, client?: OpenProjectClient): Promise<TypeItem[]>`
- Endpoint: If `projectId` provided: `GET /api/v3/projects/{projectId}/types`, else: `GET /api/v3/types`.
- Unpacks collection and extracts elements normalized via `normalizeType`.
3. `listPriorities(client?: OpenProjectClient): Promise<PriorityItem[]>`
- Endpoint: `GET /api/v3/priorities`
- Unpacks collection and extracts elements normalized via `normalizePriority`.
4. `listUsers(params?: ListUsersParams, client?: OpenProjectClient): Promise<PaginatedResult<UserItem>>`
- Endpoint: `GET /api/v3/users`
- Query Parameters: `pageSize`, `offset`.
- Response Normalization: `unpackCollection(response, normalizeUser)`.
---
## 4. Error Handling & Security
1. **Passthrough of Typed Errors**: Service methods do not swallow or alter domain-specific exceptions raised by `OpenProjectClient` (`OpenProjectNotFoundError`, `OpenProjectAuthenticationError`, `OpenProjectForbiddenError`, `OpenProjectValidationError`, `OpenProjectRateLimitError`).
2. **Credential Sanitization**: The underlying HTTP client already strips API keys from exception messages. Services maintain this safety by avoiding string concatenation containing raw configuration.
3. **Parameter Validation**: Input validation schemas will be enforced at the MCP tool boundary (Task 3 via Zod). Service functions enforce basic defensive checks (e.g. non-empty IDs).
---
## 5. Testing & Verification Strategy
### 5.1. Unit Testing with Mocked Fetch (`tests/services.test.ts`)
- Verify correct URL paths and query parameter encoding for all service functions.
- Verify fallback to `getRequestContext().client` when called inside `runWithContext`.
- Verify behavior when explicit client parameter is provided.
- Verify error propagation when mock returns 404 or 403.
### 5.2. Live Container Integration Testing
- Verify against running local OpenProject 17 container using seeded data:
- Project `mcp-test-project` (ID 4) returned by `listProjects` and `getProject`.
- Seeded work packages (IDs 38-41) returned by `listWorkPackages` and `getWorkPackage`.
- Saved query `MCP Active Tasks` (ID 30) returned by `listQueries`, `getQuery`, and `getQueryResults`.
- Taxonomies returned by `listStatuses`, `listTypes`, `listPriorities`, and `listUsers`.
---
## 6. Out of Scope (Future Tasks)
- MCP Tool registration and Zod schemas (Task 3: MCP Tool Definitions).
- Mutating operations such as creating/updating work packages (Phase 2).
- File attachment downloads and wikis (Phase 3).