init codebase
This commit is contained in:
@@ -204,6 +204,8 @@ webhook: {:id uuid, :community-id uuid, :channel-id uuid, :name string,
|
||||
mention: {:id uuid, :message-id uuid, :target-type enum, :target-id uuid?}
|
||||
notification: {:id uuid, :user-id uuid, :type enum, :source-id uuid, :read boolean}
|
||||
invite: {:id uuid, :community-id uuid, :created-by uuid, :code string, :max-uses int?, :uses int, :expires-at inst?}
|
||||
oauth-provider: {:id uuid, :provider-type enum, :name string, :client-id string, :client-secret-encrypted string, :base-url string?, :issuer-url string?, :enabled boolean, :created-at inst, :updated-at inst}
|
||||
system-setting: {:key string, :value any, :updated-at inst}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
@@ -298,3 +300,130 @@ invite: {:id uuid, :community-id uuid, :created-by uuid, :code string,
|
||||
| MD-T13 | ANSI rendering for TUI | Unit | `**bold**` → ANSI bold escape code |
|
||||
| MD-T14 | Code block in ANSI | Unit | Code block rendered with box-drawing characters |
|
||||
| MD-T15 | Mentions not processed | Unit | Markdown processor does not handle `@<user:>` (separate concern) |
|
||||
|
||||
---
|
||||
|
||||
### 2.7 Config Loader (`ajet.chat.shared.config`)
|
||||
|
||||
**Purpose:** Load, validate, and merge EDN configuration from files and environment variables. All modules use this for startup configuration.
|
||||
|
||||
**Requirements:**
|
||||
|
||||
| ID | Requirement | Priority |
|
||||
|----|-------------|----------|
|
||||
| CFG-1 | Load config from EDN file path (default: `config.edn` in classpath) | P0 |
|
||||
| CFG-2 | Deep-merge module-specific config over shared defaults | P0 |
|
||||
| CFG-3 | Override any config key via environment variables (`AJET_DB_HOST` → `{:db {:host ...}}`) | P0 |
|
||||
| CFG-4 | Validate required keys on load, throw clear error on missing/invalid config | P0 |
|
||||
| CFG-5 | Support profiles (`:dev`, `:test`, `:prod`) — merge profile-specific overrides | P1 |
|
||||
| CFG-6 | Secrets from env vars only — never log or serialize secret values | P0 |
|
||||
|
||||
**Env var mapping convention:**
|
||||
```
|
||||
AJET_DB_HOST → {:db {:host "..."}}
|
||||
AJET_DB_PORT → {:db {:port 5432}}
|
||||
AJET_DB_PASSWORD → {:db {:password "..."}}
|
||||
AJET_NATS_URL → {:nats {:url "..."}}
|
||||
AJET_OAUTH_GITHUB_CLIENT_ID → {:oauth {:github {:client-id "..."}}}
|
||||
```
|
||||
Underscores map to nested keys. Numeric strings auto-coerce to integers. `"true"`/`"false"` coerce to booleans.
|
||||
|
||||
**Shared defaults:**
|
||||
```clojure
|
||||
{:db {:host "localhost" :port 5432 :dbname "ajet_chat" :user "ajet" :pool-size 10}
|
||||
:nats {:url "nats://localhost:4222"}
|
||||
:minio {:endpoint "http://localhost:9000" :access-key "minioadmin" :secret-key "minioadmin" :bucket "ajet-chat"}}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Type | Description |
|
||||
|----|------|------|-------------|
|
||||
| CFG-T1 | Load valid EDN config | Unit | `load-config` parses EDN file and returns map |
|
||||
| CFG-T2 | Missing config file | Unit | Throws with clear error message |
|
||||
| CFG-T3 | Env var override | Unit | `AJET_DB_HOST=remote` overrides `{:db {:host "localhost"}}` |
|
||||
| CFG-T4 | Env var numeric coercion | Unit | `AJET_DB_PORT=5433` becomes integer 5433 |
|
||||
| CFG-T5 | Deep merge module config | Unit | Module config merges over defaults without clobbering sibling keys |
|
||||
| CFG-T6 | Missing required key | Unit | Missing `:db :host` throws validation error |
|
||||
| CFG-T7 | Profile merge | Unit | `:test` profile overrides `{:db {:dbname "ajet_chat_test"}}` |
|
||||
| CFG-T8 | Secrets not logged | Unit | Config with `:password` redacts value in string representation |
|
||||
|
||||
---
|
||||
|
||||
### 2.8 Logging (`ajet.chat.shared.logging`)
|
||||
|
||||
**Purpose:** Structured logging with trace ID propagation across all services.
|
||||
|
||||
**Requirements:**
|
||||
|
||||
| ID | Requirement | Priority |
|
||||
|----|-------------|----------|
|
||||
| LOG-1 | Use `clojure.tools.logging` with Logback backend | P0 |
|
||||
| LOG-2 | Structured JSON log format in production, human-readable in dev | P0 |
|
||||
| LOG-3 | Bind `trace-id` to MDC (Mapped Diagnostic Context) per request | P0 |
|
||||
| LOG-4 | Log request/response summary for every HTTP request (method, path, status, duration) | P0 |
|
||||
| LOG-5 | Log level configurable per namespace via config | P1 |
|
||||
| LOG-6 | Redact sensitive fields (passwords, tokens) in log output | P0 |
|
||||
|
||||
**MDC fields per request:**
|
||||
```
|
||||
trace-id: UUID from X-Trace-Id header
|
||||
user-id: UUID from X-User-Id header (if authenticated)
|
||||
method: HTTP method
|
||||
path: Request path
|
||||
```
|
||||
|
||||
**Log format (production):**
|
||||
```json
|
||||
{"timestamp":"2026-02-17T10:30:00Z","level":"INFO","logger":"ajet.chat.api.routes","trace-id":"uuid","user-id":"uuid","msg":"POST /api/channels/uuid/messages 201 (45ms)"}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Type | Description |
|
||||
|----|------|------|-------------|
|
||||
| LOG-T1 | Request logging middleware | Unit | Request/response logged with method, path, status, duration |
|
||||
| LOG-T2 | Trace ID in MDC | Unit | Log entries include trace-id from request header |
|
||||
| LOG-T3 | Sensitive field redaction | Unit | Password and token values replaced with `[REDACTED]` |
|
||||
| LOG-T4 | JSON format in prod | Unit | Log output parses as valid JSON in `:prod` profile |
|
||||
| LOG-T5 | Human-readable in dev | Unit | Log output is plain text with colors in `:dev` profile |
|
||||
|
||||
---
|
||||
|
||||
### 2.9 File Storage Client (`ajet.chat.shared.storage`)
|
||||
|
||||
**Purpose:** S3-compatible client for MinIO/AWS S3 file operations.
|
||||
|
||||
**Requirements:**
|
||||
|
||||
| ID | Requirement | Priority |
|
||||
|----|-------------|----------|
|
||||
| FS-1 | `upload!` — upload bytes/stream to storage with key | P0 |
|
||||
| FS-2 | `download` — get file bytes/stream by key | P0 |
|
||||
| FS-3 | `delete!` — remove file by key | P0 |
|
||||
| FS-4 | `presigned-url` — generate time-limited download URL | P1 |
|
||||
| FS-5 | Create bucket on startup if it doesn't exist | P0 |
|
||||
| FS-6 | Validate content-type (images only: JPEG, PNG, GIF, WebP) | P0 |
|
||||
| FS-7 | Validate file size (max 10MB) | P0 |
|
||||
| FS-8 | Storage key format: `attachments/{uuid}/{filename}` | P0 |
|
||||
|
||||
**Config shape:**
|
||||
```clojure
|
||||
{:minio {:endpoint "http://localhost:9000"
|
||||
:access-key "minioadmin"
|
||||
:secret-key "minioadmin"
|
||||
:bucket "ajet-chat"}}
|
||||
```
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
| ID | Test | Type | Description |
|
||||
|----|------|------|-------------|
|
||||
| FS-T1 | Upload file | Integration | `upload!` stores file, retrievable by key |
|
||||
| FS-T2 | Download file | Integration | `download` returns same bytes as uploaded |
|
||||
| FS-T3 | Delete file | Integration | `delete!` removes file, subsequent download returns nil |
|
||||
| FS-T4 | Presigned URL | Integration | Generated URL is accessible and expires |
|
||||
| FS-T5 | Invalid content-type | Unit | Non-image content-type throws validation error |
|
||||
| FS-T6 | Oversized file | Unit | File > 10MB throws validation error |
|
||||
| FS-T7 | Bucket auto-creation | Integration | On startup, creates bucket if missing |
|
||||
| FS-T8 | Upload with storage key format | Unit | Key matches `attachments/{uuid}/{filename}` pattern |
|
||||
|
||||
Reference in New Issue
Block a user