Merged in feature/eula.part1 (pull request #206)
eula support * eula support * docs
This commit is contained in:
@@ -28,6 +28,7 @@ The API is organized into the following service groups:
|
||||
| ExportService | Operations related to exports |
|
||||
| AuthService | Operations related to authentication |
|
||||
| AdminService | Operations related to user management and administration |
|
||||
| EulaService | Operations related to End User License Agreement management and tracking |
|
||||
|
||||
## Authentication and Authorization
|
||||
|
||||
@@ -1247,6 +1248,281 @@ Re-enables a previously disabled user in AWS Cognito. Restores authentication ca
|
||||
|
||||
---
|
||||
|
||||
## EULA Operations (EulaService)
|
||||
|
||||
The EULA (End User License Agreement) service manages EULA versions and tracks user consent. It provides both public endpoints for retrieving and agreeing to EULAs, and administrative endpoints for managing EULA versions and viewing compliance reports.
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
#### `GET /eula`
|
||||
|
||||
Returns the current active EULA version. This endpoint is public and does not require authentication.
|
||||
|
||||
**Security**: Public (no authentication required)
|
||||
|
||||
**Response** (`EulaPublicResponse`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "019580df-ef65-7676-8de9-94435a93337a",
|
||||
"version": "1.0",
|
||||
"title": "Terms of Service",
|
||||
"content": "# Terms of Service\n\nThis is the full EULA content in Markdown format...",
|
||||
"effective_date": "2025-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Responses**:
|
||||
|
||||
- `200`: Current EULA version retrieved successfully
|
||||
- `404`: No current EULA version configured
|
||||
- `500`: Internal server error
|
||||
|
||||
#### `GET /eula/status`
|
||||
|
||||
Returns the current user's agreement status for the current EULA version. Used by the frontend to determine if the user needs to be prompted to agree.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Response** (`EulaStatusResponse`):
|
||||
|
||||
```json
|
||||
{
|
||||
"has_agreed": true,
|
||||
"current_version": "1.0",
|
||||
"current_version_id": "019580df-ef65-7676-8de9-94435a93337a",
|
||||
"agreed_at": "2025-01-15T10:30:00Z",
|
||||
"agreed_version": "1.0"
|
||||
}
|
||||
```
|
||||
|
||||
**Responses**:
|
||||
|
||||
- `200`: EULA status retrieved successfully
|
||||
- `401`: Authentication required
|
||||
- `404`: No current EULA version configured
|
||||
- `500`: Internal server error
|
||||
|
||||
#### `POST /eula/agree`
|
||||
|
||||
Records the user's agreement to the current EULA version. This operation is idempotent - calling it multiple times will not create duplicate records. The IP address is automatically captured from the request.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Response** (`EulaAgreementResponse`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "019580df-ef65-7676-8de9-94435a93337b",
|
||||
"eula_version_id": "019580df-ef65-7676-8de9-94435a93337a",
|
||||
"eula_version": "1.0",
|
||||
"agreed_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Responses**:
|
||||
|
||||
- `201`: Agreement recorded successfully (first agreement)
|
||||
- `200`: User already agreed to current version (returns existing agreement)
|
||||
- `400`: No current EULA version configured
|
||||
- `401`: Authentication required
|
||||
- `500`: Internal server error
|
||||
|
||||
### Administrative Endpoints
|
||||
|
||||
#### `GET /admin/eula`
|
||||
|
||||
Returns a paginated list of all EULA versions.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| page | integer | 1 | Page number (1-10000) |
|
||||
| page_size | integer | 20 | Results per page (1-100) |
|
||||
|
||||
**Response** (`EulaVersionList`):
|
||||
|
||||
```json
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"id": "019580df-ef65-7676-8de9-94435a93337a",
|
||||
"version": "1.0",
|
||||
"title": "Terms of Service",
|
||||
"content": "# Terms of Service...",
|
||||
"effective_date": "2025-01-01T00:00:00Z",
|
||||
"is_current": true,
|
||||
"created_at": "2024-12-01T00:00:00Z",
|
||||
"created_by": "admin@example.com",
|
||||
"activated_at": "2025-01-01T00:00:00Z",
|
||||
"activated_by": "admin@example.com"
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /admin/eula`
|
||||
|
||||
Creates a new EULA version. The content should be in Markdown format.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Request Body** (`EulaVersionCreate`):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"title": "Updated Terms of Service",
|
||||
"content": "# Updated Terms of Service\n\nThis is the new EULA content...",
|
||||
"effective_date": "2025-06-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ----- | ---- | -------- | ----------- |
|
||||
| version | string | Yes | Unique version identifier (max 50 chars) |
|
||||
| title | string | Yes | Human-readable title |
|
||||
| content | string | Yes | Full EULA text in Markdown format |
|
||||
| effective_date | datetime | Yes | When this version becomes effective |
|
||||
|
||||
**Response** (`201 Created`): `EulaVersion` object
|
||||
|
||||
**Error Responses**:
|
||||
|
||||
- `400`: Invalid request data
|
||||
- `401`: Authentication required
|
||||
- `403`: Insufficient permissions
|
||||
- `409`: Version string already exists
|
||||
|
||||
#### `GET /admin/eula/{version_id}`
|
||||
|
||||
Retrieves a specific EULA version by its ID.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Path Parameters**:
|
||||
|
||||
- `version_id`: EULA version UUID
|
||||
|
||||
**Response** (`EulaVersion`): Full EULA version object
|
||||
|
||||
#### `PATCH /admin/eula/{version_id}`
|
||||
|
||||
Updates only the metadata (title and effective date) of an EULA version. Content cannot be modified to maintain audit integrity.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Request Body** (`EulaVersionUpdate`):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Updated Title",
|
||||
"effective_date": "2025-07-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (`200 OK`): Updated `EulaVersion` object
|
||||
|
||||
#### `POST /admin/eula/{version_id}/activate`
|
||||
|
||||
Sets this version as the current active EULA. Uses a database transaction to atomically clear the previous current flag and set the new one.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Path Parameters**:
|
||||
|
||||
- `version_id`: EULA version UUID to activate
|
||||
|
||||
**Response** (`200 OK`): Activated `EulaVersion` object with `is_current=true`
|
||||
|
||||
**Error Responses**:
|
||||
|
||||
- `404`: Version not found
|
||||
- `409`: Concurrent activation conflict (another admin activated a different version)
|
||||
|
||||
#### `GET /admin/eula/agreements`
|
||||
|
||||
Returns a paginated list of user agreements. Use filters to view agreements for specific EULA versions or users.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| page | integer | 1 | Page number (1-10000) |
|
||||
| page_size | integer | 20 | Results per page (1-100) |
|
||||
| version_id | uuid | - | Filter by specific EULA version |
|
||||
| user_id | string | - | Filter by Cognito subject ID |
|
||||
|
||||
**Response** (`EulaAgreementList`):
|
||||
|
||||
```json
|
||||
{
|
||||
"agreements": [
|
||||
{
|
||||
"id": "019580df-ef65-7676-8de9-94435a93337b",
|
||||
"cognito_subject_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"user_email": "user@example.com",
|
||||
"eula_version_id": "019580df-ef65-7676-8de9-94435a93337a",
|
||||
"eula_version": "1.0",
|
||||
"agreed_at": "2025-01-15T10:30:00Z",
|
||||
"agreed_from_ip": "192.168.1.1"
|
||||
}
|
||||
],
|
||||
"total": 150,
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /admin/eula/compliance`
|
||||
|
||||
Returns a comprehensive compliance report showing which users have and have not agreed to a specific EULA version. Defaults to the current active version if no version_id is provided.
|
||||
|
||||
**Security**: Requires JWT authentication
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| page | integer | 1 | Page number (1-10000) |
|
||||
| page_size | integer | 50 | Results per page (1-100) |
|
||||
| version_id | uuid | - | Specific EULA version (defaults to current) |
|
||||
| agreed | boolean | - | Filter by agreement status |
|
||||
|
||||
**Response** (`EulaComplianceReport`):
|
||||
|
||||
```json
|
||||
{
|
||||
"version_id": "019580df-ef65-7676-8de9-94435a93337a",
|
||||
"version": "1.0",
|
||||
"total_users": 200,
|
||||
"agreed_count": 150,
|
||||
"not_agreed_count": 50,
|
||||
"compliance_percentage": 75.0,
|
||||
"users": [
|
||||
{
|
||||
"cognito_subject_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"email": "user@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"has_agreed": true,
|
||||
"agreed_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 50,
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request/Response Schemas
|
||||
|
||||
### Standard Response Headers
|
||||
@@ -1330,13 +1606,14 @@ The API uses Permit.io for fine-grained authorization. This section documents wh
|
||||
|
||||
The following endpoints skip authentication and Permit.io authorization entirely:
|
||||
|
||||
| Endpoint | Purpose |
|
||||
| ---------------- | ------------------------ |
|
||||
| `GET /home` | Public home/menu page |
|
||||
| `GET /logout` | Session termination |
|
||||
| `GET /health` | Health check endpoint |
|
||||
| `GET /swagger/*` | Swagger UI documentation |
|
||||
| `GET /metrics` | Prometheus metrics |
|
||||
| Endpoint | Purpose |
|
||||
| ---------------- | --------------------------------- |
|
||||
| `GET /home` | Public home/menu page |
|
||||
| `GET /logout` | Session termination |
|
||||
| `GET /health` | Health check endpoint |
|
||||
| `GET /swagger/*` | Swagger UI documentation |
|
||||
| `GET /metrics` | Prometheus metrics |
|
||||
| `GET /eula` | Public EULA version retrieval |
|
||||
|
||||
### Special Authentication Endpoints
|
||||
|
||||
@@ -1404,6 +1681,17 @@ The following table shows what data is passed to Permit.io for each protected en
|
||||
| `DELETE` | `/admin/users/{email}` | `admin` | `delete` | Delete user |
|
||||
| `POST` | `/admin/users/{email}/disable` | `admin` | `post` | Disable user |
|
||||
| `POST` | `/admin/users/{email}/enable` | `admin` | `post` | Enable user |
|
||||
| **EULA Operations** |
|
||||
| `GET` | `/eula` | - | - | Public (no auth) |
|
||||
| `GET` | `/eula/status` | `eula` | `get` | Get user's EULA status |
|
||||
| `POST` | `/eula/agree` | `eula` | `post` | Record EULA agreement |
|
||||
| `GET` | `/admin/eula` | `admin` | `get` | List EULA versions |
|
||||
| `POST` | `/admin/eula` | `admin` | `post` | Create EULA version |
|
||||
| `GET` | `/admin/eula/{version_id}` | `admin` | `get` | Get EULA version |
|
||||
| `PATCH` | `/admin/eula/{version_id}` | `admin` | `patch` | Update EULA metadata |
|
||||
| `POST` | `/admin/eula/{version_id}/activate` | `admin` | `post` | Activate EULA version |
|
||||
| `GET` | `/admin/eula/agreements` | `admin` | `get` | List EULA agreements |
|
||||
| `GET` | `/admin/eula/compliance` | `admin` | `get` | Get compliance report |
|
||||
|
||||
### Permit.io Configuration Requirements
|
||||
|
||||
@@ -1422,7 +1710,8 @@ To configure Permit.io to work with this API, administrators must create:
|
||||
| `field-extractions` | Field extraction operations |
|
||||
| `query` | Query definition management |
|
||||
| `export` | Export operations |
|
||||
| `admin` | User administration |
|
||||
| `admin` | User administration and EULA admin operations |
|
||||
| `eula` | EULA status and agreement operations |
|
||||
|
||||
#### Actions
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ erDiagram
|
||||
collectorVersions ||--|| collectorActiveVersions : "tracks active"
|
||||
collectorQueries }o--|| queries : "references"
|
||||
|
||||
eulaVersions ||--o{ eulaAgreements : "has agreements"
|
||||
|
||||
clients {
|
||||
varchar clientId PK
|
||||
text name UK
|
||||
@@ -292,6 +294,28 @@ erDiagram
|
||||
int addedVersion FK
|
||||
int removedVersion FK
|
||||
}
|
||||
|
||||
eulaVersions {
|
||||
uuid id PK
|
||||
varchar version UK
|
||||
text title
|
||||
text content
|
||||
timestamptz effectiveDate
|
||||
timestamptz createdAt
|
||||
varchar createdBy
|
||||
boolean isCurrent
|
||||
timestamptz activatedAt
|
||||
varchar activatedBy
|
||||
}
|
||||
|
||||
eulaAgreements {
|
||||
uuid id PK
|
||||
varchar cognitoSubjectId
|
||||
varchar userEmail
|
||||
uuid eulaVersionId FK
|
||||
timestamptz agreedAt
|
||||
varchar agreedFromIp
|
||||
}
|
||||
```
|
||||
|
||||
## Core Entities
|
||||
@@ -497,6 +521,72 @@ CREATE INDEX idx_documentlabels_document_label_time ON documentLabels(documentId
|
||||
- Multiple applications of same label are allowed (historical tracking)
|
||||
- Note: Added in migration 110
|
||||
|
||||
### EULA System
|
||||
|
||||
The EULA (End User License Agreement) system tracks EULA versions and user consent. Added in migration 121.
|
||||
|
||||
#### EULA Versions (`eulaVersions`)
|
||||
Stores EULA version records with content and activation tracking.
|
||||
|
||||
```sql
|
||||
CREATE TABLE eulaVersions (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
version VARCHAR(50) NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
effectiveDate TIMESTAMPTZ NOT NULL,
|
||||
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
createdBy VARCHAR(320) NOT NULL,
|
||||
isCurrent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
activatedAt TIMESTAMPTZ,
|
||||
activatedBy VARCHAR(320)
|
||||
);
|
||||
|
||||
-- Partial unique index enforces only ONE row can have isCurrent=true at any time
|
||||
CREATE UNIQUE INDEX idx_eulaversions_one_current ON eulaVersions(isCurrent) WHERE isCurrent = TRUE;
|
||||
|
||||
-- Indexes for efficient queries
|
||||
CREATE INDEX idx_eulaversions_effectivedate ON eulaVersions(effectiveDate DESC);
|
||||
CREATE INDEX idx_eulaversions_iscurrent ON eulaVersions(isCurrent);
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
- UUID v7 primary keys for time-ordered insertion
|
||||
- Unique version string constraint prevents duplicate versions
|
||||
- Content stored in Markdown format for flexible rendering
|
||||
- Partial unique index ensures only one version can be `isCurrent=true` at any time
|
||||
- Activation tracking with timestamp and admin email
|
||||
- Effective date supports scheduling future EULA versions
|
||||
|
||||
#### EULA Agreements (`eulaAgreements`)
|
||||
Tracks user consent with audit metadata.
|
||||
|
||||
```sql
|
||||
CREATE TABLE eulaAgreements (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
cognitoSubjectId VARCHAR(256) NOT NULL,
|
||||
userEmail VARCHAR(320) NOT NULL,
|
||||
eulaVersionId uuid NOT NULL REFERENCES eulaVersions(id) ON DELETE RESTRICT,
|
||||
agreedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
agreedFromIp VARCHAR(45) NOT NULL,
|
||||
UNIQUE(cognitoSubjectId, eulaVersionId)
|
||||
);
|
||||
|
||||
-- Indexes for efficient queries
|
||||
CREATE INDEX idx_eulaagreements_subjectid ON eulaAgreements(cognitoSubjectId);
|
||||
CREATE INDEX idx_eulaagreements_versionid ON eulaAgreements(eulaVersionId);
|
||||
CREATE INDEX idx_eulaagreements_version_time ON eulaAgreements(eulaVersionId, agreedAt DESC);
|
||||
CREATE INDEX idx_eulaagreements_user_time ON eulaAgreements(cognitoSubjectId, agreedAt DESC);
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
- Links users (by Cognito subject ID) to EULA versions they've agreed to
|
||||
- Unique constraint on (cognitoSubjectId, eulaVersionId) prevents duplicate agreements
|
||||
- Stores user email at time of agreement for audit purposes
|
||||
- IP address tracking for legal compliance
|
||||
- ON DELETE RESTRICT prevents deleting EULA versions that have agreements
|
||||
- Composite indexes optimize common query patterns (agreements by version, user history)
|
||||
|
||||
### Queries (`queries`)
|
||||
Defines data extraction logic with type-specific implementations.
|
||||
|
||||
@@ -1675,6 +1765,7 @@ Similar triggers exist for `collectorMinCleanVersions` and `collectorMinTextVers
|
||||
- **Migration 114**: Added `documentFieldExtractionArrayFields` table with 112 array fields
|
||||
- **Migration 115**: Added `currentFieldExtractions` and `currentFieldExtractionsWithArrayCount` views
|
||||
- **Migration 116**: Added `documentUploads.folder_id` column with foreign key constraint
|
||||
- **Migration 121**: Added `eulaVersions` and `eulaAgreements` tables for EULA management and consent tracking
|
||||
|
||||
### Version Compatibility
|
||||
- Backward-compatible schema changes prioritized
|
||||
@@ -1728,8 +1819,9 @@ Views are layered for reusability:
|
||||
- **Core entities**: 5 (clients, documents, batch_uploads, queries, results)
|
||||
- **Organization tables**: 3 (folders, labels, documentLabels)
|
||||
- **Field extraction tables**: 3 (documentFieldExtractions, documentFieldExtractionVersions, documentFieldExtractionArrayFields)
|
||||
- **EULA tables**: 2 (eulaVersions, eulaAgreements)
|
||||
- **Supporting tables**: 16 (versions, entries, configs, dependencies, etc.)
|
||||
- **Total tables**: 27
|
||||
- **Total tables**: 29
|
||||
|
||||
### View Count
|
||||
- **Query management**: 6 views
|
||||
|
||||
@@ -315,7 +315,8 @@ Permissions follow the format `resource:action`:
|
||||
| `folders` | `get`, `post`, `patch`, `delete` | Folder management |
|
||||
| `query` | `get`, `post`, `patch` | Query definitions |
|
||||
| `export` | `get`, `post` | Export operations |
|
||||
| `admin` | `get`, `post`, `patch`, `delete` | User administration |
|
||||
| `admin` | `get`, `post`, `patch`, `delete` | User and EULA administration |
|
||||
| `eula` | `get`, `post` | EULA status and agreement |
|
||||
|
||||
### Using Permissions in UI
|
||||
|
||||
@@ -888,6 +889,9 @@ Content-Type: application/json
|
||||
| `/client/{id}/document/batch` | POST | Upload ZIP batch |
|
||||
| `/document/{id}` | GET | Get document details |
|
||||
| `/clients/{clientId}/folders` | GET | List folders |
|
||||
| `/eula` | GET | Get current EULA (no auth required) |
|
||||
| `/eula/status` | GET | Check user's EULA agreement status |
|
||||
| `/eula/agree` | POST | Record EULA agreement |
|
||||
|
||||
### Public Endpoints (No Authentication Required)
|
||||
|
||||
@@ -896,6 +900,7 @@ Content-Type: application/json
|
||||
| `/health` | Health check and version info |
|
||||
| `/swagger/*` | API documentation |
|
||||
| `/metrics` | Prometheus metrics |
|
||||
| `/eula` | Current EULA version (public) |
|
||||
|
||||
### TypeScript Type Definitions
|
||||
|
||||
|
||||
@@ -39,6 +39,14 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
| /admin/users/{email} | GET, HEAD, PATCH, DELETE | AdminService |
|
||||
| /admin/users/{email}/disable | POST | AdminService |
|
||||
| /admin/users/{email}/enable | POST | AdminService |
|
||||
| /eula | GET | EulaService |
|
||||
| /eula/status | GET | EulaService |
|
||||
| /eula/agree | POST | EulaService |
|
||||
| /admin/eula | GET, POST | EulaService |
|
||||
| /admin/eula/{version_id} | GET, PATCH | EulaService |
|
||||
| /admin/eula/{version_id}/activate | POST | EulaService |
|
||||
| /admin/eula/agreements | GET | EulaService |
|
||||
| /admin/eula/compliance | GET | EulaService |
|
||||
|
||||
---
|
||||
|
||||
@@ -299,6 +307,56 @@ This document provides a comprehensive summary of all REST API endpoints availab
|
||||
- **Idempotent**: Safe to call multiple times
|
||||
- Returns: `{success, email, action, cognito_subject_id, timestamp}` (200 OK)
|
||||
|
||||
## EULA Service
|
||||
|
||||
### Public Endpoints
|
||||
|
||||
- **GET /eula** - Get current EULA version (public, no authentication required)
|
||||
- Returns: `{id, version, title, content, effective_date}` (200 OK)
|
||||
- Returns 404 if no current EULA is configured
|
||||
|
||||
- **GET /eula/status** - Check user's agreement status
|
||||
- Requires authentication
|
||||
- Returns: `{has_agreed, current_version, current_version_id, agreed_at?, agreed_version?}`
|
||||
|
||||
- **POST /eula/agree** - Record user agreement to current EULA
|
||||
- Requires authentication
|
||||
- IP address automatically captured from request
|
||||
- **Idempotent**: Returns 200 if already agreed, 201 for new agreement
|
||||
- Returns: `{id, eula_version_id, eula_version, agreed_at}`
|
||||
|
||||
### Administrative Endpoints
|
||||
|
||||
- **GET /admin/eula** - List all EULA versions with pagination
|
||||
- Query Parameters: `page`, `page_size`
|
||||
- Returns: `{versions[], total, has_more}`
|
||||
|
||||
- **POST /admin/eula** - Create a new EULA version
|
||||
- Request Body: `{version, title, content, effective_date}`
|
||||
- Content should be in Markdown format
|
||||
- Returns: Created `EulaVersion` object (201 Created)
|
||||
|
||||
- **GET /admin/eula/{version_id}** - Get specific EULA version
|
||||
- Returns: Full `EulaVersion` object
|
||||
|
||||
- **PATCH /admin/eula/{version_id}** - Update EULA metadata
|
||||
- Request Body: `{title?, effective_date?}`
|
||||
- Note: Content cannot be modified (audit integrity)
|
||||
- Returns: Updated `EulaVersion` object
|
||||
|
||||
- **POST /admin/eula/{version_id}/activate** - Activate EULA version
|
||||
- Sets this version as current (atomically clears previous)
|
||||
- Returns: Activated `EulaVersion` with `is_current=true`
|
||||
|
||||
- **GET /admin/eula/agreements** - List EULA agreements
|
||||
- Query Parameters: `page`, `page_size`, `version_id?`, `user_id?`
|
||||
- Returns: `{agreements[], total, has_more}`
|
||||
|
||||
- **GET /admin/eula/compliance** - Get compliance report
|
||||
- Query Parameters: `page`, `page_size`, `version_id?`, `agreed?`
|
||||
- Defaults to current EULA version
|
||||
- Returns: `{version_id, version, total_users, agreed_count, not_agreed_count, compliance_percentage, users[], page, page_size, has_more}`
|
||||
|
||||
## Common Response Codes
|
||||
|
||||
All endpoints may return the following standard HTTP response codes:
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
# QueryAPI Software Architecture Guide
|
||||
|
||||
This document provides a comprehensive guide to the QueryAPI software architecture for new engineers joining the project. It explains the purpose of each architectural layer, how components interact, and what logic belongs where when adding new features.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Component Layers](#component-layers)
|
||||
- [Request Flow](#request-flow)
|
||||
- [Adding a New API Endpoint](#adding-a-new-api-endpoint)
|
||||
- [Key Patterns](#key-patterns)
|
||||
- [Component Diagrams](#component-diagrams)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
QueryAPI follows a layered architecture pattern with clear separation of concerns:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph External["External Layer"]
|
||||
Client[HTTP Client]
|
||||
OpenAPI[OpenAPI Spec<br/>serviceAPIs/queryAPI.yaml]
|
||||
end
|
||||
|
||||
subgraph Entry["Entry Point"]
|
||||
Main[cmd/queryAPI/main.go]
|
||||
end
|
||||
|
||||
subgraph Server["Server Layer"]
|
||||
Listener[internal/server/api/listener.go]
|
||||
Echo[Echo HTTP Router]
|
||||
end
|
||||
|
||||
subgraph Middleware["Middleware Layer"]
|
||||
RateLimit[Rate Limiting]
|
||||
JWT[JWT Auth Middleware]
|
||||
TokenValidation[Token Validation]
|
||||
PermitIO[Permit.io Authorization]
|
||||
OAPIValidation[OpenAPI Request Validation]
|
||||
end
|
||||
|
||||
subgraph Generated["Generated Layer"]
|
||||
APIGen[api/queryAPI/api.gen.go<br/>ServerInterface + Types]
|
||||
RegisterHandlers[RegisterHandlers]
|
||||
end
|
||||
|
||||
subgraph Controllers["Controller Layer"]
|
||||
Controllers_impl[api/queryAPI/*.go<br/>Controllers struct]
|
||||
end
|
||||
|
||||
subgraph Services["Service Layer"]
|
||||
ServiceImpl[internal/<domain>/service.go<br/>Business Logic]
|
||||
end
|
||||
|
||||
subgraph Data["Data Access Layer"]
|
||||
Repository[internal/database/repository/*.sql.go<br/>SQLC Generated]
|
||||
Queries[internal/database/queries/*.sql<br/>SQL Definitions]
|
||||
Migrations[internal/database/migrations/*.sql<br/>Schema Changes]
|
||||
end
|
||||
|
||||
subgraph Infrastructure["Infrastructure"]
|
||||
DB[(PostgreSQL)]
|
||||
S3[(AWS S3)]
|
||||
SQS[(AWS SQS)]
|
||||
end
|
||||
|
||||
Client --> Main
|
||||
OpenAPI -.->|generates| APIGen
|
||||
Main --> Listener
|
||||
Listener --> Echo
|
||||
Echo --> RateLimit --> JWT --> TokenValidation --> PermitIO --> OAPIValidation
|
||||
OAPIValidation --> RegisterHandlers
|
||||
RegisterHandlers --> Controllers_impl
|
||||
Controllers_impl --> ServiceImpl
|
||||
ServiceImpl --> Repository
|
||||
Queries -.->|generates| Repository
|
||||
Repository --> DB
|
||||
ServiceImpl --> S3
|
||||
ServiceImpl --> SQS
|
||||
```
|
||||
|
||||
## Component Layers
|
||||
|
||||
### 1. Entry Point (`cmd/queryAPI/main.go`)
|
||||
|
||||
**Purpose**: Application bootstrap and dependency wiring.
|
||||
|
||||
**Responsibilities**:
|
||||
- Print version information
|
||||
- Initialize configuration from environment variables
|
||||
- Create and wire all service dependencies
|
||||
- Register handlers with the router
|
||||
- Start the HTTP server
|
||||
|
||||
**Key Code Pattern**:
|
||||
```go
|
||||
// Create services
|
||||
cli := client.New(cfg)
|
||||
doc := document.New(cfg)
|
||||
|
||||
// Wire services into Controllers
|
||||
services := &queryapi.Services{
|
||||
Client: cli,
|
||||
Document: doc,
|
||||
}
|
||||
cons := queryapi.NewControllers(services, cfg)
|
||||
|
||||
// Register routes
|
||||
queryapi.RegisterHandlers(cfg.Router, cons)
|
||||
```
|
||||
|
||||
**When to modify**: When adding a new domain service that needs to be injected into the API layer.
|
||||
|
||||
---
|
||||
|
||||
### 2. Server Layer (`internal/server/api/listener.go`)
|
||||
|
||||
**Purpose**: HTTP server configuration, middleware setup, and lifecycle management.
|
||||
|
||||
**Responsibilities**:
|
||||
- Create and configure the Echo HTTP router
|
||||
- Register middleware in the correct order
|
||||
- Set up health and metrics endpoints
|
||||
- Initialize background task runners
|
||||
- Handle graceful shutdown
|
||||
|
||||
**Key Components**:
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `Server` struct | Holds router, port, and cleanup functions |
|
||||
| `New()` function | Creates server with all middleware configured |
|
||||
| `Listen()` method | Starts the HTTP server |
|
||||
| Middleware chain | Rate limiting -> Auth -> Validation |
|
||||
|
||||
**Middleware Order** (applied in sequence):
|
||||
1. Config injection middleware
|
||||
2. Prometheus metrics
|
||||
3. Request logging (slog)
|
||||
4. Recovery (panic handling)
|
||||
5. CORS
|
||||
6. Debug logging (when DEBUG=true)
|
||||
7. Rate limiting
|
||||
8. Authentication routes registration
|
||||
9. Test user injection (when DISABLE_AUTH=true)
|
||||
10. Handler registration
|
||||
11. OpenAPI request validation
|
||||
|
||||
---
|
||||
|
||||
### 3. Middleware Layer (`internal/cognitoauth/`)
|
||||
|
||||
**Purpose**: Authentication and authorization enforcement.
|
||||
|
||||
**Key Files**:
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `middleware.go` | Main middleware functions (`TokenValidationMiddleware`, `JWTAuthMiddleware`) |
|
||||
| `handler.go` | OAuth callback and login flow handlers |
|
||||
| `permitio.go` | Permit.io authorization client |
|
||||
| `token.go` | JWT token operations (refresh, validation) |
|
||||
| `validation.go` | Token signature and claims validation |
|
||||
|
||||
**Authentication Flow**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant M as Middleware
|
||||
participant J as JWKS
|
||||
participant P as Permit.io
|
||||
|
||||
C->>M: Request with JWT
|
||||
M->>M: Check if public path
|
||||
alt Public Path
|
||||
M->>C: Allow (skip auth)
|
||||
else Protected Path
|
||||
M->>M: Extract token from header/cookie
|
||||
M->>J: Verify token signature
|
||||
J-->>M: Token valid
|
||||
M->>M: Extract user claims
|
||||
M->>P: Check permission(user, action, resource)
|
||||
P-->>M: Permitted/Denied
|
||||
alt Permitted
|
||||
M->>C: Allow request
|
||||
else Denied
|
||||
M->>C: 403 Forbidden
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Path Types**:
|
||||
- **Public paths**: `/health`, `/swagger/*`, `/metrics`, `/eula` (GET only)
|
||||
- **Auth-only paths**: `/identity`, `/eula/status`, `/eula/agree` (authenticated but no specific permissions)
|
||||
- **Protected paths**: All other routes (require both authentication and Permit.io authorization)
|
||||
|
||||
---
|
||||
|
||||
### 4. Generated API Layer (`api/queryAPI/api.gen.go`)
|
||||
|
||||
**Purpose**: Type-safe API interface generated from OpenAPI specification.
|
||||
|
||||
**Generated by**: `oapi-codegen` from `serviceAPIs/queryAPI.yaml`
|
||||
|
||||
**Key Generated Components**:
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `ServerInterface` | Interface that controllers must implement |
|
||||
| Request/Response types | Strongly-typed DTOs (e.g., `ClientCreate`, `DocClient`) |
|
||||
| `RegisterHandlers()` | Connects routes to handler methods |
|
||||
| `ServerInterfaceWrapper` | Wraps handlers with parameter extraction |
|
||||
| Parameter types | Path/query parameter types (e.g., `ClientID`) |
|
||||
|
||||
**Example Interface Method**:
|
||||
```go
|
||||
// From ServerInterface
|
||||
CreateClient(ctx echo.Context) error
|
||||
GetClient(ctx echo.Context, id ClientID) error
|
||||
```
|
||||
|
||||
**Important**: Never edit `api.gen.go` directly. Modify `serviceAPIs/queryAPI.yaml` and run `task generate`.
|
||||
|
||||
---
|
||||
|
||||
### 5. Controller Layer (`api/queryAPI/*.go`)
|
||||
|
||||
**Purpose**: HTTP request handling and response formatting.
|
||||
|
||||
**Key Files**:
|
||||
| File | Domain |
|
||||
|------|--------|
|
||||
| `controllers.go` | `Controllers` struct and constructor |
|
||||
| `client.go` | Client endpoints |
|
||||
| `documents.go` | Document endpoints |
|
||||
| `folders.go` | Folder endpoints |
|
||||
| `labels.go` | Label endpoints |
|
||||
| `collector.go` | Collector endpoints |
|
||||
| `export.go` | Export endpoints |
|
||||
| `authHandlers.go` | Authentication-related endpoints |
|
||||
| `adminHandlers.go` | User administration endpoints |
|
||||
|
||||
**Responsibilities**:
|
||||
- Bind and validate request bodies
|
||||
- Call appropriate service methods
|
||||
- Transform service results to API response types
|
||||
- Return appropriate HTTP status codes
|
||||
- Handle errors with proper HTTP error responses
|
||||
|
||||
**Controller Pattern**:
|
||||
```go
|
||||
func (s *Controllers) GetClient(ctx echo.Context, id ClientID) error {
|
||||
// 1. Call service layer
|
||||
client, err := s.svc.Client.Get(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
// 2. Handle error
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
fmt.Sprintf("Unable to get client: %s", err))
|
||||
}
|
||||
|
||||
// 3. Transform to API type and return
|
||||
return ctx.JSON(http.StatusOK, DocClient{
|
||||
Id: client.ID,
|
||||
Name: client.Name,
|
||||
CanSync: client.CanSync,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**What belongs here**:
|
||||
- Request parsing and binding
|
||||
- Input validation (beyond OpenAPI validation)
|
||||
- Calling service methods
|
||||
- Response transformation
|
||||
- HTTP status code decisions
|
||||
|
||||
**What does NOT belong here**:
|
||||
- Business logic
|
||||
- Database operations
|
||||
- External service calls
|
||||
- Complex data transformations
|
||||
|
||||
---
|
||||
|
||||
### 6. Service Layer (`internal/<domain>/`)
|
||||
|
||||
**Purpose**: Business logic and domain operations.
|
||||
|
||||
**Structure per domain**:
|
||||
```
|
||||
internal/client/
|
||||
├── service.go # Service struct and constructor
|
||||
├── create.go # Create operation
|
||||
├── get.go # Read operations
|
||||
├── list.go # List operations
|
||||
├── status.go # Status checks
|
||||
└── *_test.go # Unit tests
|
||||
```
|
||||
|
||||
**Service Pattern**:
|
||||
```go
|
||||
// Service struct with config dependency
|
||||
type Service struct {
|
||||
cfg serviceconfig.ConfigProvider
|
||||
}
|
||||
|
||||
// Constructor
|
||||
func New(cfg serviceconfig.ConfigProvider) *Service {
|
||||
return &Service{cfg}
|
||||
}
|
||||
|
||||
// Business operation
|
||||
func (s *Service) Get(ctx context.Context, id string) (*Client, error) {
|
||||
// Access database through config
|
||||
client, err := s.cfg.GetDBQueries().GetClient(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseFullClient(client), nil
|
||||
}
|
||||
```
|
||||
|
||||
**What belongs here**:
|
||||
- Business logic and rules
|
||||
- Input normalization and validation
|
||||
- Orchestration of multiple repository calls
|
||||
- Transaction management
|
||||
- Domain-specific transformations
|
||||
- Business error handling
|
||||
|
||||
**Accessing Infrastructure**:
|
||||
```go
|
||||
// Database queries
|
||||
s.cfg.GetDBQueries().GetClient(ctx, id)
|
||||
|
||||
// Database transactions
|
||||
s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
||||
// Multiple operations in a transaction
|
||||
return nil
|
||||
})
|
||||
|
||||
// Object storage
|
||||
s.cfg.GetStoreClient() // S3 client
|
||||
s.cfg.GetBucket() // Bucket name
|
||||
|
||||
// Queue operations
|
||||
s.cfg.GetQueueClient()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Repository Layer (`internal/database/repository/`)
|
||||
|
||||
**Purpose**: Type-safe database access generated from SQL.
|
||||
|
||||
**Generated by**: SQLC from `internal/database/queries/*.sql`
|
||||
|
||||
**Key Files**:
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `db.go` | `Queries` struct and `DBTX` interface |
|
||||
| `models.go` | Generated Go types from database schema |
|
||||
| `client.sql.go` | Generated client query methods |
|
||||
| `document.sql.go` | Generated document query methods |
|
||||
| etc. | One file per SQL source file |
|
||||
|
||||
**SQL Query Definition** (`internal/database/queries/client.sql`):
|
||||
```sql
|
||||
-- name: GetClient :one
|
||||
SELECT * FROM fullClients WHERE clientId = $1;
|
||||
|
||||
-- name: CreateClient :exec
|
||||
INSERT INTO clients (clientId, name) VALUES ($1, $2);
|
||||
|
||||
-- name: ListClients :many
|
||||
SELECT * FROM fullClients ORDER BY name;
|
||||
```
|
||||
|
||||
**Generated Go Code**:
|
||||
```go
|
||||
func (q *Queries) GetClient(ctx context.Context, clientid string) (*Fullclient, error)
|
||||
func (q *Queries) CreateClient(ctx context.Context, arg *CreateClientParams) error
|
||||
func (q *Queries) ListClients(ctx context.Context) ([]*Fullclient, error)
|
||||
```
|
||||
|
||||
**Transaction Support**:
|
||||
```go
|
||||
// Create queries with transaction
|
||||
q.WithTx(tx)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Database Migrations (`internal/database/migrations/`)
|
||||
|
||||
**Purpose**: Version-controlled schema evolution.
|
||||
|
||||
**Naming Convention**: `YYYYMMDDHHMMSS_description.up.sql` / `.down.sql`
|
||||
|
||||
**Examples**:
|
||||
- `00000000000003_clients.up.sql` - Create clients table
|
||||
- `00000000000109_create_folders_table.up.sql` - Add folders feature
|
||||
|
||||
**Migration Tool**: golang-migrate
|
||||
|
||||
---
|
||||
|
||||
### 9. Configuration Layer (`internal/serviceconfig/`)
|
||||
|
||||
**Purpose**: Environment-based configuration with dependency injection.
|
||||
|
||||
**Key Interface** (`ConfigProvider`):
|
||||
```go
|
||||
type ConfigProvider interface {
|
||||
// Database access
|
||||
GetDBQueries() *repository.Queries
|
||||
GetDBPool() *pgxpool.Pool
|
||||
ExecuteDBTransaction(ctx, func) error
|
||||
|
||||
// Logging
|
||||
GetLogger() *slog.Logger
|
||||
|
||||
// AWS services
|
||||
GetStoreClient() objectstore.Client // S3
|
||||
GetQueueClient() queue.Client // SQS
|
||||
GetBucket() string
|
||||
|
||||
// Auth
|
||||
GetAuthConfig() auth.ConfigProvider
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Composition**:
|
||||
```go
|
||||
type QueryAPIConfig struct {
|
||||
api.BaseConfig // Server config
|
||||
clientsync.ClientSyncConfig // Queue config
|
||||
objectstore.ObjectStoreConfig // S3 config
|
||||
BackgroundRunner *backgroundtask.Runner
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Flow
|
||||
|
||||
Here's how a request flows through the system:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as HTTP Client
|
||||
participant E as Echo Router
|
||||
participant MW as Middleware Chain
|
||||
participant V as OpenAPI Validator
|
||||
participant W as ServerInterfaceWrapper
|
||||
participant H as Controller Handler
|
||||
participant S as Service
|
||||
participant R as Repository
|
||||
participant DB as PostgreSQL
|
||||
|
||||
C->>E: POST /client
|
||||
E->>MW: Apply middleware
|
||||
MW->>MW: Rate limit check
|
||||
MW->>MW: JWT validation
|
||||
MW->>MW: Permit.io authorization
|
||||
MW->>V: Validate request body
|
||||
V->>W: Route to wrapper
|
||||
W->>W: Extract path/query params
|
||||
W->>H: CreateClient(ctx)
|
||||
H->>H: ctx.Bind(&req)
|
||||
H->>S: client.Create(ctx, params)
|
||||
S->>S: Normalize input
|
||||
S->>R: CreateClient(ctx, dbParams)
|
||||
R->>DB: INSERT INTO clients...
|
||||
DB-->>R: Success
|
||||
R-->>S: nil error
|
||||
S-->>H: clientID, nil
|
||||
H->>H: Build response
|
||||
H-->>C: 201 Created + JSON body
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a New API Endpoint
|
||||
|
||||
Follow these steps to add a new endpoint (e.g., `GET /client/{id}/summary`):
|
||||
|
||||
### Step 1: Define the API Contract
|
||||
|
||||
Edit `serviceAPIs/queryAPI.yaml`:
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
/client/{id}/summary:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ClientID"
|
||||
get:
|
||||
operationId: getClientSummary
|
||||
tags:
|
||||
- ClientService
|
||||
summary: Get client summary
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Client summary
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ClientSummary"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ClientSummary:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- documentCount
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
documentCount:
|
||||
type: integer
|
||||
```
|
||||
|
||||
### Step 2: Generate Code
|
||||
|
||||
```bash
|
||||
task generate
|
||||
```
|
||||
|
||||
This updates:
|
||||
- `api/queryAPI/api.gen.go` - Adds `GetClientSummary` to `ServerInterface` and `ClientSummary` type
|
||||
|
||||
### Step 3: Add Database Query (if needed)
|
||||
|
||||
Edit `internal/database/queries/client.sql`:
|
||||
|
||||
```sql
|
||||
-- name: GetClientDocumentCount :one
|
||||
SELECT COUNT(*) as count FROM documents WHERE clientId = $1;
|
||||
```
|
||||
|
||||
Run: `task generate`
|
||||
|
||||
### Step 4: Add Service Method
|
||||
|
||||
Create or edit `internal/client/summary.go`:
|
||||
|
||||
```go
|
||||
package client
|
||||
|
||||
import "context"
|
||||
|
||||
type Summary struct {
|
||||
ID string
|
||||
DocumentCount int64
|
||||
}
|
||||
|
||||
func (s *Service) GetSummary(ctx context.Context, id string) (*Summary, error) {
|
||||
// Validate client exists
|
||||
client, err := s.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get document count
|
||||
count, err := s.cfg.GetDBQueries().GetClientDocumentCount(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get document count: %w", err)
|
||||
}
|
||||
|
||||
return &Summary{
|
||||
ID: client.ID,
|
||||
DocumentCount: count,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Implement Handler
|
||||
|
||||
Edit `api/queryAPI/client.go`:
|
||||
|
||||
```go
|
||||
func (s *Controllers) GetClientSummary(ctx echo.Context, id ClientID) error {
|
||||
summary, err := s.svc.Client.GetSummary(ctx.Request().Context(), id)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
fmt.Sprintf("Unable to get client summary: %s", err))
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, ClientSummary{
|
||||
Id: summary.ID,
|
||||
DocumentCount: int32(summary.DocumentCount),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Add Tests
|
||||
|
||||
Create `internal/client/summary_test.go`:
|
||||
|
||||
```go
|
||||
func TestService_GetSummary(t *testing.T) {
|
||||
// Use testcontainers for real database testing
|
||||
cfg := test.NewTestConfig(t)
|
||||
svc := client.New(cfg)
|
||||
|
||||
// Create test client
|
||||
clientID, err := svc.Create(ctx, client.CreateParams{...})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test summary
|
||||
summary, err := svc.GetSummary(ctx, clientID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, clientID, summary.ID)
|
||||
assert.Equal(t, int64(0), summary.DocumentCount)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: Verify
|
||||
|
||||
```bash
|
||||
task lint # Check for issues
|
||||
task test:unit # Run unit tests
|
||||
task fullsuite:ci # Run full test suite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Dependency Injection via ConfigProvider
|
||||
|
||||
Services receive all dependencies through the `ConfigProvider` interface:
|
||||
|
||||
```go
|
||||
type Service struct {
|
||||
cfg serviceconfig.ConfigProvider
|
||||
}
|
||||
|
||||
func (s *Service) DoSomething(ctx context.Context) error {
|
||||
// Database
|
||||
s.cfg.GetDBQueries().SomeQuery(ctx, ...)
|
||||
|
||||
// Logging
|
||||
s.cfg.GetLogger().Info("Operation completed")
|
||||
|
||||
// S3
|
||||
s.cfg.GetStoreClient().PutObject(...)
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction Handling
|
||||
|
||||
Use `ExecuteDBTransaction` for operations requiring atomicity:
|
||||
|
||||
```go
|
||||
err := s.cfg.ExecuteDBTransaction(ctx, func(ctx context.Context, q *repository.Queries) error {
|
||||
if err := q.CreateClient(ctx, params); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.CreateRootFolder(ctx, folderParams); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Services return errors, controllers convert to HTTP errors:
|
||||
|
||||
```go
|
||||
// Service - return domain errors
|
||||
if client == nil {
|
||||
return nil, errors.New("client not found")
|
||||
}
|
||||
|
||||
// Controller - convert to HTTP error
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, err.Error())
|
||||
}
|
||||
```
|
||||
|
||||
### Input Normalization
|
||||
|
||||
Normalize and validate inputs in the service layer:
|
||||
|
||||
```go
|
||||
func (s *Service) normalizeCreate(params *CreateParams) error {
|
||||
// Trim whitespace
|
||||
params.Name = strings.TrimSpace(params.Name)
|
||||
|
||||
// Validate format
|
||||
if !isValidID(params.ID) {
|
||||
return errors.New("invalid ID format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Diagrams
|
||||
|
||||
### Domain Package Structure
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph "internal/client/"
|
||||
S[service.go<br/>Service struct]
|
||||
C[create.go<br/>Create operation]
|
||||
G[get.go<br/>Read operations]
|
||||
L[list.go<br/>List operations]
|
||||
ST[status.go<br/>Status checks]
|
||||
end
|
||||
|
||||
subgraph "Repository"
|
||||
R[repository/client.sql.go]
|
||||
end
|
||||
|
||||
S --> C
|
||||
S --> G
|
||||
S --> L
|
||||
S --> ST
|
||||
C --> R
|
||||
G --> R
|
||||
L --> R
|
||||
ST --> R
|
||||
```
|
||||
|
||||
### Configuration Composition
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ConfigProvider {
|
||||
<<interface>>
|
||||
GetDBQueries()
|
||||
GetLogger()
|
||||
GetStoreClient()
|
||||
}
|
||||
|
||||
class BaseConfig {
|
||||
Port int
|
||||
BaseURL string
|
||||
CognitoConfig
|
||||
DBConfig
|
||||
AWSConfig
|
||||
}
|
||||
|
||||
class QueryAPIConfig {
|
||||
BaseConfig
|
||||
ClientSyncConfig
|
||||
ObjectStoreConfig
|
||||
}
|
||||
|
||||
ConfigProvider <|.. BaseConfig
|
||||
BaseConfig <|-- QueryAPIConfig
|
||||
```
|
||||
|
||||
### Service Layer Interactions
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Controllers
|
||||
CH[Client Handler]
|
||||
DH[Document Handler]
|
||||
end
|
||||
|
||||
subgraph Services
|
||||
CS[Client Service]
|
||||
DS[Document Service]
|
||||
US[Upload Service]
|
||||
end
|
||||
|
||||
subgraph Repository
|
||||
CR[Client Queries]
|
||||
DR[Document Queries]
|
||||
end
|
||||
|
||||
subgraph Infrastructure
|
||||
DB[(PostgreSQL)]
|
||||
S3[(S3)]
|
||||
end
|
||||
|
||||
CH --> CS
|
||||
DH --> DS
|
||||
DH --> US
|
||||
CS --> CR
|
||||
DS --> DR
|
||||
US --> DR
|
||||
US --> S3
|
||||
CR --> DB
|
||||
DR --> DB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Layer | Location | Responsibility |
|
||||
|-------|----------|----------------|
|
||||
| Entry Point | `cmd/queryAPI/main.go` | Bootstrap, wire dependencies |
|
||||
| Server | `internal/server/api/listener.go` | HTTP server, middleware setup |
|
||||
| Middleware | `internal/cognitoauth/` | Auth, authorization |
|
||||
| Generated | `api/queryAPI/api.gen.go` | Interface, types (don't edit) |
|
||||
| Controllers | `api/queryAPI/*.go` | Request handling, response formatting |
|
||||
| Services | `internal/<domain>/` | Business logic |
|
||||
| Repository | `internal/database/repository/` | Database access (generated) |
|
||||
| Queries | `internal/database/queries/*.sql` | SQL definitions |
|
||||
| Migrations | `internal/database/migrations/` | Schema changes |
|
||||
| Config | `internal/serviceconfig/` | Environment config, DI |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [02-service-architecture.md](./02-service-architecture.md) - Overall system architecture
|
||||
- [06-code-organization.md](./06-code-organization.md) - Project structure details
|
||||
- [03-api-documentation.md](./03-api-documentation.md) - API reference
|
||||
- [04-data-architecture.md](./04-data-architecture.md) - Database schema
|
||||
Reference in New Issue
Block a user