diff --git a/package.json b/package.json index 1bb2655..52621ee 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "kill-port": "^2.0.1", "prettier": "^3.6.2", "typescript": "^5.9.2", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "tsx": "^4.7.0", + "@types/node": "^20.11.0" } } diff --git a/tests/api/api-requirements-v2.md b/tests/api/api-requirements-v2.md deleted file mode 100644 index 9eac434..0000000 --- a/tests/api/api-requirements-v2.md +++ /dev/null @@ -1,840 +0,0 @@ -# Banatie REST API Implementation Plan - -**Version:** 2.0 -**Status:** Ready for Implementation -**Executor:** Claude Code -**Database Schema:** v2.0 (banatie-database-design.md) - ---- - -## Overview - -REST API for Banatie image generation service. All endpoints use `/api/v1/` prefix for versioning. - -**Core Features:** -- AI image generation with Google Gemini Flash -- Dual alias system (project-scoped + flow-scoped) -- Technical aliases (@last, @first, @upload) -- Flow-based generation chains -- Live generation endpoint with caching -- Upload and reference images - -**Authentication:** API keys only (`bnt_` prefix) - ---- - -## Authentication - -All endpoints require API key in header: - -``` -X-API-Key: bnt_xxx... -``` - -**API Key Types:** -- `master`: Full access to all projects in organization -- `project`: Access to specific project only - -**Unauthorized Response (401):** -```json -{ - "error": "Unauthorized", - "message": "Invalid or missing API key" -} -``` - ---- - -## Implementation Phases - -### Phase 1: Foundation -**Goal:** Core utilities and services - -**Tasks:** -- Create TypeScript type definitions for all models -- Build validation utilities (alias format, pagination, query params) -- Build helper utilities (pagination, hash, query helpers) -- Create `AliasService` with 3-tier resolution (technical → flow → project) - -**Git Commit:** -``` -feat: add foundation utilities and alias service -``` - ---- - -### Phase 2: Core Generation Flow -**Goal:** Main generation endpoints - -**Services:** -- `ImageService` - CRUD operations with soft delete -- `GenerationService` - Full lifecycle management - -**Endpoints:** -- `POST /api/v1/generations` - Create with reference images & dual aliases -- `GET /api/v1/generations` - List with filters -- `GET /api/v1/generations/:id` - Get details with related data - -**Git Commit:** -``` -feat: implement core generation endpoints -``` - ---- - -### Phase 3: Flow Management -**Goal:** Flow operations - -**Services:** -- `FlowService` - CRUD with computed counts & alias management - -**Endpoints:** -- `POST /api/v1/flows` - Create flow -- `GET /api/v1/flows` - List flows with computed counts -- `GET /api/v1/flows/:id` - Get details with generations and images -- `PUT /api/v1/flows/:id/aliases` - Update flow aliases -- `DELETE /api/v1/flows/:id/aliases/:alias` - Remove specific alias -- `DELETE /api/v1/flows/:id` - Delete flow - -**Git Commit:** -``` -feat: implement flow management endpoints -``` - ---- - -### Phase 4: Enhanced Image Management -**Goal:** Complete image operations - -**Endpoints:** -- `POST /api/v1/images/upload` - Upload with alias, flow, metadata -- `GET /api/v1/images` - List with filters -- `GET /api/v1/images/:id` - Get details with usage info -- `GET /api/v1/images/resolve/:alias` - Resolve alias with precedence -- `PUT /api/v1/images/:id` - Update metadata -- `DELETE /api/v1/images/:id` - Soft/hard delete - -**Git Commit:** -``` -feat: implement image management endpoints -``` - ---- - -### Phase 5: Generation Refinements -**Goal:** Additional generation operations - -**Endpoints:** -- `POST /api/v1/generations/:id/retry` - Retry failed generation -- `DELETE /api/v1/generations/:id` - Delete generation - -**Git Commit:** -``` -feat: add generation retry and delete endpoints -``` - ---- - -### Phase 6: Live Generation -**Goal:** URL-based generation with caching - -**Services:** -- `PromptCacheService` - SHA-256 caching with hit tracking - -**Endpoints:** -- `GET /api/v1/live` - Generate image via URL with streaming proxy - -**Important:** Stream image directly from MinIO (no 302 redirect) for better performance. - -**Git Commit:** -``` -feat: implement live generation endpoint with caching -``` - ---- - -### Phase 7: Analytics -**Goal:** Project statistics and metrics - -**Services:** -- `AnalyticsService` - Aggregation queries - -**Endpoints:** -- `GET /api/v1/analytics/summary` - Project statistics -- `GET /api/v1/analytics/generations/timeline` - Time-series data - -**Git Commit:** -``` -feat: add analytics endpoints -``` - ---- - -### Phase 8: Testing & Documentation -**Goal:** Quality assurance - -**Tasks:** -- Unit tests for all services (target >80% coverage) -- Integration tests for critical flows -- Error handling consistency review -- Update API documentation - -**Git Commit:** -``` -test: add comprehensive test coverage and documentation -``` - ---- - -## API Endpoints Specification - -### GENERATIONS - -#### POST /api/v1/generations - -Create new image generation. - -**Request Body:** -```typescript -{ - prompt: string; // Required: 1-2000 chars - aspectRatio?: string; // Optional: '16:9', '1:1', '4:3', '9:16' - width?: number; // Optional: 1-8192 - height?: number; // Optional: 1-8192 - referenceImages?: string[]; // Optional: ['@logo', '@product', '@last'] - flowId?: string; // Optional: Add to existing flow - assignAlias?: string; // Optional: Project-scoped alias '@brand' - assignFlowAlias?: string; // Optional: Flow-scoped alias '@hero' (requires flowId) - meta?: Record; -} -``` - -**Response (200):** -```typescript -{ - generation: Generation; - image?: Image; // If generation completed -} -``` - -**Errors:** 400, 401, 404, 422, 429, 500 - ---- - -#### GET /api/v1/generations - -List generations with filtering. - -**Query Params:** -```typescript -{ - flowId?: string; - status?: 'pending' | 'processing' | 'success' | 'failed'; - limit?: number; // Default: 20, max: 100 - offset?: number; // Default: 0 - sortBy?: 'createdAt' | 'updatedAt'; - order?: 'asc' | 'desc'; // Default: desc -} -``` - -**Response (200):** -```typescript -{ - generations: Generation[]; - pagination: PaginationInfo; -} -``` - ---- - -#### GET /api/v1/generations/:id - -Get generation details. - -**Response (200):** -```typescript -{ - generation: Generation; - image?: Image; - referencedImages: Image[]; - flow?: FlowSummary; -} -``` - ---- - -#### POST /api/v1/generations/:id/retry - -Retry failed generation. - -**Response (200):** -```typescript -{ - generation: Generation; // New generation with incremented retry_count -} -``` - -**Errors:** 404, 422 - ---- - -#### DELETE /api/v1/generations/:id - -Delete generation. - -**Query Params:** -```typescript -{ - hard?: boolean; // Default: false -} -``` - -**Response (204):** No content - ---- - -### IMAGES - -#### POST /api/v1/images/upload - -Upload image file. - -**Request:** multipart/form-data - -**Fields:** -```typescript -{ - file: File; // Required, max 5MB - alias?: string; // Project-scoped: '@logo' - flowAlias?: string; // Flow-scoped: '@hero' (requires flowId) - flowId?: string; - description?: string; - tags?: string[]; // JSON array as string - focalPoint?: string; // JSON: '{"x":0.5,"y":0.5}' - meta?: string; // JSON object as string -} -``` - -**Response (201):** -```typescript -{ - image: Image; - flow?: FlowSummary; // If flowAlias assigned -} -``` - -**Errors:** 400, 409, 422 - ---- - -#### GET /api/v1/images - -List images. - -**Query Params:** -```typescript -{ - flowId?: string; - source?: 'generated' | 'uploaded'; - alias?: string; - limit?: number; // Default: 20, max: 100 - offset?: number; - sortBy?: 'createdAt' | 'fileSize'; - order?: 'asc' | 'desc'; -} -``` - -**Response (200):** -```typescript -{ - images: Image[]; - pagination: PaginationInfo; -} -``` - ---- - -#### GET /api/v1/images/:id - -Get image details. - -**Response (200):** -```typescript -{ - image: Image; - generation?: Generation; - usedInGenerations: GenerationSummary[]; -} -``` - ---- - -#### GET /api/v1/images/resolve/:alias - -Resolve alias to image. - -**Query Params:** -```typescript -{ - flowId?: string; // Provide flow context -} -``` - -**Response (200):** -```typescript -{ - image: Image; - scope: 'flow' | 'project' | 'technical'; - flow?: FlowSummary; -} -``` - -**Resolution Order:** -1. Technical aliases (@last, @first, @upload) if flowId provided -2. Flow aliases from flows.aliases if flowId provided -3. Project aliases from images.alias - -**Errors:** 404 - ---- - -#### PUT /api/v1/images/:id - -Update image metadata. - -**Request Body:** -```typescript -{ - alias?: string; - description?: string; - tags?: string[]; - focalPoint?: { x: number; y: number }; - meta?: Record; -} -``` - -**Response (200):** -```typescript -{ - image: Image; -} -``` - -**Errors:** 404, 409, 422 - ---- - -#### DELETE /api/v1/images/:id - -Delete image. - -**Query Params:** -```typescript -{ - hard?: boolean; // Default: false -} -``` - -**Response (204):** No content - ---- - -### FLOWS - -#### POST /api/v1/flows - -Create new flow. - -**Request Body:** -```typescript -{ - meta?: Record; -} -``` - -**Response (201):** -```typescript -{ - flow: Flow; -} -``` - ---- - -#### GET /api/v1/flows - -List flows. - -**Query Params:** -```typescript -{ - limit?: number; // Default: 20, max: 100 - offset?: number; - sortBy?: 'createdAt' | 'updatedAt'; - order?: 'asc' | 'desc'; -} -``` - -**Response (200):** -```typescript -{ - flows: Array; - pagination: PaginationInfo; -} -``` - ---- - -#### GET /api/v1/flows/:id - -Get flow details. - -**Response (200):** -```typescript -{ - flow: Flow; - generations: Generation[]; // Ordered by created_at ASC - images: Image[]; - resolvedAliases: Record; -} -``` - ---- - -#### PUT /api/v1/flows/:id/aliases - -Update flow aliases. - -**Request Body:** -```typescript -{ - aliases: Record; // { "@hero": "image-uuid" } -} -``` - -**Response (200):** -```typescript -{ - flow: Flow; -} -``` - -**Validation:** -- Keys must match `^@[a-zA-Z0-9_-]+$` -- Values must be valid image UUIDs -- Cannot use reserved: @last, @first, @upload - -**Errors:** 404, 422 - ---- - -#### DELETE /api/v1/flows/:id/aliases/:alias - -Remove specific alias from flow. - -**Response (204):** No content - -**Errors:** 404 - ---- - -#### DELETE /api/v1/flows/:id - -Delete flow. - -**Response (204):** No content - -**Note:** Cascades to images, sets NULL on generations.flow_id - ---- - -### LIVE GENERATION - -#### GET /api/v1/live - -Generate image via URL with caching and streaming. - -**Query Params:** -```typescript -{ - prompt: string; // Required - aspectRatio?: string; - width?: number; - height?: number; - reference?: string | string[]; // '@logo' or ['@logo','@style'] -} -``` - -**Response:** Image stream with headers - -**Headers:** -``` -Content-Type: image/jpeg -Cache-Control: public, max-age=31536000 -X-Cache-Status: HIT | MISS -``` - -**Implementation:** -1. Compute cache key: SHA256(prompt + sorted params) -2. Check prompt_url_cache table -3. If HIT: increment hit_count, stream from MinIO -4. If MISS: generate, cache, stream from MinIO -5. Stream image bytes directly (no 302 redirect) - -**Errors:** 400, 404, 500 - ---- - -### ANALYTICS - -#### GET /api/v1/analytics/summary - -Get project statistics. - -**Query Params:** -```typescript -{ - startDate?: string; // ISO 8601 - endDate?: string; - flowId?: string; -} -``` - -**Response (200):** -```typescript -{ - period: { startDate: string; endDate: string }; - metrics: { - totalGenerations: number; - successfulGenerations: number; - failedGenerations: number; - successRate: number; - totalImages: number; - uploadedImages: number; - generatedImages: number; - avgProcessingTimeMs: number; - totalCacheHits: number; - cacheHitRate: number; - totalCost: number; - }; - flows: FlowSummary[]; -} -``` - ---- - -#### GET /api/v1/analytics/generations/timeline - -Get generation statistics over time. - -**Query Params:** -```typescript -{ - startDate?: string; - endDate?: string; - flowId?: string; - groupBy?: 'hour' | 'day' | 'week'; // Default: day -} -``` - -**Response (200):** -```typescript -{ - data: Array<{ - timestamp: string; - total: number; - successful: number; - failed: number; - avgProcessingTimeMs: number; - }>; -} -``` - ---- - -## Implementation Guidelines - -### Alias Resolution Algorithm - -**Priority Order:** -1. Technical aliases (@last, @first, @upload) - compute from flow data -2. Flow-scoped aliases - from flows.aliases JSONB -3. Project-scoped aliases - from images.alias column - -**Technical Aliases:** -- `@last`: Latest generation output in flow (any status) -- `@first`: First generation output in flow -- `@upload`: Latest uploaded image in flow - -### Dual Alias Assignment - -When creating generation or uploading image: -- `assignAlias` → set images.alias (project scope) -- `assignFlowAlias` → add to flows.aliases (flow scope) -- Both can be assigned simultaneously - -### Flow Updates - -Update `flows.updated_at` on: -- New generation created with flowId -- New image uploaded with flowId -- Flow aliases modified - -### Audit Trail - -Track `api_key_id` in: -- `images.api_key_id` - who uploaded/generated -- `generations.api_key_id` - who requested - -### Rate Limiting - -In-memory rate limiting (defer Redis for MVP): -- Master key: 1000 req/hour, 100 generations/hour -- Project key: 500 req/hour, 50 generations/hour - -**Headers:** -``` -X-RateLimit-Limit: 500 -X-RateLimit-Remaining: 487 -X-RateLimit-Reset: 1698765432 -``` - -### Error Response Format - -```typescript -{ - error: string; - message: string; - details?: unknown; - requestId?: string; -} -``` - -### MinIO Integration - -Use streaming for `/api/v1/live`: -```typescript -const stream = await minioClient.getObject(bucket, storageKey); -res.set('Content-Type', mimeType); -stream.pipe(res); -``` - -Generate presigned URLs for other endpoints: -```typescript -const url = await minioClient.presignedGetObject(bucket, storageKey, 24 * 60 * 60); -``` - ---- - -## Validation Rules - -**Alias Format:** -- Pattern: `^@[a-zA-Z0-9_-]+$` -- Reserved: @last, @first, @upload -- Length: 3-100 chars - -**File Upload:** -- Max size: 5MB -- MIME types: image/jpeg, image/png, image/webp -- Max dimensions: 8192x8192 - -**Prompt:** -- Min: 1 char -- Max: 2000 chars - -**Aspect Ratio:** -- Pattern: `^\d+:\d+$` -- Examples: 16:9, 1:1, 4:3, 9:16 - ---- - -## Service Architecture - -### Core Services - -**AliasService:** -- Resolve aliases with 3-tier precedence -- Compute technical aliases -- Validate alias format - -**ImageService:** -- CRUD operations -- Soft delete support -- Usage tracking - -**GenerationService:** -- Generation lifecycle -- Status transitions -- Error handling -- Retry logic - -**FlowService:** -- Flow CRUD -- Alias management -- Computed counts - -**PromptCacheService:** -- Cache key computation (SHA-256) -- Hit tracking -- Cache lookup - -**AnalyticsService:** -- Aggregation queries -- Time-series grouping - -### Reusable Utilities - -**Validators:** -- Alias format -- Pagination params -- Query filters - -**Helpers:** -- Pagination builder -- SHA-256 hashing -- Query helpers - ---- - -## Testing Requirements - -**Unit Tests:** -- All services must have unit tests -- Target coverage: >80% -- Mock database calls - -**Integration Tests:** -- Critical flows end-to-end -- Real database transactions -- API endpoint testing with supertest - -**Test Scenarios:** -- Alias resolution precedence -- Flow-scoped vs project-scoped aliases -- Technical alias computation -- Dual alias assignment -- Cache hit/miss behavior -- Error handling -- Rate limiting - ---- - -## Success Criteria - -✅ All endpoints functional per specification -✅ >80% test coverage on services -✅ Consistent error handling across all endpoints -✅ All validation rules implemented -✅ Rate limiting working -✅ Documentation updated -✅ Git commits after each phase - ---- - -*Document Version: 2.0* -*Created: 2025-11-09* -*Target: Claude Code Implementation* -*Database Schema: v2.0* diff --git a/tests/api/fixture/test-image.png b/tests/api/fixture/test-image.png new file mode 100644 index 0000000..1660cdb Binary files /dev/null and b/tests/api/fixture/test-image.png differ diff --git a/tests/api/test-config.ts b/tests/api/test-config.ts index 741b825..46d1dd1 100644 --- a/tests/api/test-config.ts +++ b/tests/api/test-config.ts @@ -2,17 +2,17 @@ export const config = { // API Configuration - baseURL: process.env.API_BASE_URL || 'http://localhost:3000', - apiKey: process.env.API_KEY || 'bnt_test_key_change_me', - + baseURL: 'http://localhost:3000', + apiKey: 'bnt_71e7e16732ac5e21f597edc56e99e8c3696e713552ec9d1f44dfeffb2ef7c495', + // Paths resultsDir: '../../results', fixturesDir: './fixtures', - + // Timeouts requestTimeout: 30000, generationTimeout: 60000, - + // Test settings verbose: true, saveImages: true, diff --git a/tests/api/test-image.png b/tests/api/test-image.png deleted file mode 100644 index dcf7a3a..0000000 Binary files a/tests/api/test-image.png and /dev/null differ