From a92b1bf48288c5bb94c089e8bc1226fb00428941 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Mon, 17 Nov 2025 23:32:13 +0700 Subject: [PATCH] docs: phase 3 part 4 - comprehensive JSDoc documentation for all v1 API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete JSDoc documentation to all API v1 endpoints with detailed descriptions, parameters, returns, error codes, and usage examples (Section 12). **Generation Endpoints (generations.ts):** - POST / - Create generation with references, aliases, and enhancement - GET / - List with flow/status filtering and pagination - GET /:id - Get single generation with full relations - PUT /:id - Update with auto-regeneration on prompt/aspectRatio change - POST /:id/regenerate - Regenerate with exact same parameters - POST /:id/retry - Legacy endpoint (deprecated, delegates to regenerate) - DELETE /:id - Delete with alias protection rules **Flow Endpoints (flows.ts):** - GET / - List flows with computed counts and pagination - GET /:id - Get single flow with generationCount and imageCount - GET /:id/generations - List flow's generations with pagination - GET /:id/images - List flow's images with pagination - PUT /:id/aliases - Update flow-scoped aliases (JSONB merge) - DELETE /:id/aliases/:alias - Remove specific flow alias - POST /:id/regenerate - Regenerate most recent generation in flow - DELETE /:id - Hard delete flow (generations/images remain) **Image Endpoints (images.ts):** - POST /upload - Upload with flowId logic (undefined=auto, null=none, string=specific) - GET / - List with flow/source/alias filtering and pagination - GET /resolve/:alias - 3-tier resolution (technical → flow → project) - GET /:id - Get single image with complete details - PUT /:id - Update focal point and metadata - PUT /:id/alias - Assign project-scoped alias (Section 6.1) - DELETE /:id - Hard delete with MinIO cleanup and cascades **CDN Endpoints (cdn.ts):** - GET /:orgSlug/:projectSlug/img/:filenameOrAlias - Public image serving - Supports filename and @alias access - Long-term browser caching (1 year) - No authentication required - GET /:orgSlug/:projectSlug/live/:scope - Live URL generation - Automatic prompt-based caching - IP rate limiting (10/hour, cache hits excluded) - Lazy scope creation - Scope generation limits - Cache key from prompt + params - X-Cache-Status: HIT|MISS headers **Scope Management Endpoints (scopes.ts):** - POST / - Create scope manually with settings - GET / - List with currentGenerations stats and pagination - GET /:slug - Get single scope by slug with stats - PUT /:slug - Update settings (allowNewGenerations, limits) - POST /:slug/regenerate - Regenerate specific image or all in scope - DELETE /:slug - Delete scope with cascading image deletion **JSDoc Standards Applied:** - Complete function descriptions with use cases - @route method and path - @authentication requirements and key types - @rateLimit specifications where applicable - @param detailed parameter descriptions with types and optionality - @returns status codes with response descriptions - @throws error codes with explanations - @example realistic usage examples with request/response samples - @deprecated tags for legacy endpoints **Documentation Features:** - Comprehensive parameter descriptions - Default values clearly indicated - Type information for all parameters - Error code documentation - Multiple examples per complex endpoint - Response header documentation - Cache behavior explanations - Flow logic documentation - Alias resolution precedence details - Rate limiting specifics All JSDoc follows standardized format for consistent API documentation across the entire v1 API surface. Zero new TypeScript errors introduced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/api-service/src/routes/cdn.ts | 128 ++++++++-- apps/api-service/src/routes/v1/flows.ts | 204 +++++++++++++-- apps/api-service/src/routes/v1/generations.ts | 223 +++++++++++++++-- apps/api-service/src/routes/v1/images.ts | 232 +++++++++++++++++- apps/api-service/src/routes/v1/scopes.ts | 192 ++++++++++++++- 5 files changed, 908 insertions(+), 71 deletions(-) diff --git a/apps/api-service/src/routes/cdn.ts b/apps/api-service/src/routes/cdn.ts index 4777be2..4a7c52c 100644 --- a/apps/api-service/src/routes/cdn.ts +++ b/apps/api-service/src/routes/cdn.ts @@ -39,11 +39,46 @@ const getLiveScopeService = (): LiveScopeService => { }; /** - * GET /cdn/:orgSlug/:projectSlug/img/:filenameOrAlias + * Serve images by filename or project-scoped alias via public CDN * - * Serve images by filename or project-scoped alias (Section 8) - * Public endpoint - no authentication required - * Returns image bytes with caching headers + * Public CDN endpoint for serving images without authentication: + * - Supports filename-based access (exact match in storageKey) + * - Supports project-scoped alias access (@alias-name) + * - Returns raw image bytes with optimal caching headers + * - Long-term browser caching (1 year max-age) + * - No rate limiting (public access) + * + * URL structure matches MinIO storage organization for efficient lookups. + * + * @route GET /cdn/:orgSlug/:projectSlug/img/:filenameOrAlias + * @authentication None - Public endpoint + * + * @param {string} req.params.orgSlug - Organization slug + * @param {string} req.params.projectSlug - Project slug + * @param {string} req.params.filenameOrAlias - Filename or @alias + * + * @returns {Buffer} 200 - Image file bytes with Content-Type header + * @returns {object} 404 - Organization, project, or image not found + * @returns {object} 500 - CDN or storage error + * + * @throws {Error} ORG_NOT_FOUND - Organization does not exist + * @throws {Error} PROJECT_NOT_FOUND - Project does not exist + * @throws {Error} IMAGE_NOT_FOUND - Image not found + * @throws {Error} CDN_ERROR - General CDN error + * + * @example + * // Access by filename + * GET /cdn/acme/website/img/hero-background.jpg + * + * @example + * // Access by alias + * GET /cdn/acme/website/img/@hero + * + * Response Headers: + * Content-Type: image/jpeg + * Content-Length: 245810 + * Cache-Control: public, max-age=31536000 + * X-Image-Id: 550e8400-e29b-41d4-a716-446655440000 */ cdnRouter.get( '/:orgSlug/:projectSlug/img/:filenameOrAlias', @@ -145,18 +180,83 @@ cdnRouter.get( ); /** - * GET /cdn/:orgSlug/:projectSlug/live/:scope + * Live URL generation with automatic caching and scope management * - * Live URL endpoint with caching (Section 8.3) - * Public endpoint - no authentication required - * Query params: prompt, aspectRatio, autoEnhance, template + * Public endpoint for on-demand image generation via URL parameters: + * - No authentication required (public access) + * - Automatic prompt-based caching (cache key computed from params) + * - IP-based rate limiting (10 new generations per hour) + * - Scope-based generation limits + * - Lazy scope creation (automatic on first use) + * - Returns raw image bytes with caching headers * - * Flow: - * 1. Resolve org, project, and scope - * 2. Compute cache key from params - * 3. Check if image exists in cache (via meta field) - * 4. If HIT: return cached image - * 5. If MISS: check scope limits, generate new image, cache, return + * Cache behavior: + * - Cache HIT: Returns existing image instantly, no rate limit check + * - Cache MISS: Generates new image, counts toward IP rate limit + * - Cache key computed from: prompt + aspectRatio + autoEnhance + template + * - Cached images stored with meta.isLiveUrl = true + * + * Scope management: + * - Scopes separate generation budgets (e.g., "hero", "gallery") + * - Auto-created on first use if allowNewLiveScopes = true + * - Generation limits per scope (default: 30) + * - Scope stats tracked (currentGenerations, lastGeneratedAt) + * + * @route GET /cdn/:orgSlug/:projectSlug/live/:scope + * @authentication None - Public endpoint + * @rateLimit 10 new generations per hour per IP (cache hits excluded) + * + * @param {string} req.params.orgSlug - Organization slug + * @param {string} req.params.projectSlug - Project slug + * @param {string} req.params.scope - Scope identifier (alphanumeric + hyphens + underscores) + * @param {string} req.query.prompt - Image description (required) + * @param {string} [req.query.aspectRatio='1:1'] - Aspect ratio (1:1, 16:9, 3:2, 9:16) + * @param {boolean} [req.query.autoEnhance=false] - Enable prompt enhancement + * @param {string} [req.query.template] - Enhancement template (photorealistic, illustration, etc.) + * + * @returns {Buffer} 200 - Image file bytes with headers + * @returns {object} 400 - Missing/invalid prompt or scope format + * @returns {object} 403 - Scope creation disabled + * @returns {object} 404 - Organization or project not found + * @returns {object} 429 - Rate limit or scope generation limit exceeded + * @returns {object} 500 - Generation or storage error + * + * @throws {Error} VALIDATION_ERROR - Prompt is required + * @throws {Error} SCOPE_INVALID_FORMAT - Invalid scope format + * @throws {Error} ORG_NOT_FOUND - Organization does not exist + * @throws {Error} PROJECT_NOT_FOUND - Project does not exist + * @throws {Error} SCOPE_CREATION_DISABLED - New scope creation not allowed + * @throws {Error} SCOPE_GENERATION_LIMIT_EXCEEDED - Scope limit reached + * @throws {Error} IP_RATE_LIMIT_EXCEEDED - IP rate limit exceeded + * @throws {Error} LIVE_URL_ERROR - General generation error + * + * @example + * // Basic generation with caching + * GET /cdn/acme/website/live/hero-section?prompt=mountain+landscape&aspectRatio=16:9 + * + * @example + * // With auto-enhancement + * GET /cdn/acme/website/live/gallery?prompt=product+photo&autoEnhance=true&template=product + * + * Response Headers (Cache HIT): + * Content-Type: image/jpeg + * Content-Length: 245810 + * Cache-Control: public, max-age=31536000 + * X-Cache-Status: HIT + * X-Scope: hero-section + * X-Image-Id: 550e8400-e29b-41d4-a716-446655440000 + * + * Response Headers (Cache MISS): + * Content-Type: image/jpeg + * Content-Length: 198234 + * Cache-Control: public, max-age=31536000 + * X-Cache-Status: MISS + * X-Scope: hero-section + * X-Generation-Id: 660e8400-e29b-41d4-a716-446655440001 + * X-Image-Id: 770e8400-e29b-41d4-a716-446655440002 + * X-RateLimit-Limit: 10 + * X-RateLimit-Remaining: 9 + * X-RateLimit-Reset: 3456 */ cdnRouter.get( '/:orgSlug/:projectSlug/live/:scope', diff --git a/apps/api-service/src/routes/v1/flows.ts b/apps/api-service/src/routes/v1/flows.ts index a9ca0d5..4f37d59 100644 --- a/apps/api-service/src/routes/v1/flows.ts +++ b/apps/api-service/src/routes/v1/flows.ts @@ -69,8 +69,29 @@ const getGenerationService = (): GenerationService => { // ); /** - * GET /api/v1/flows - * List all flows for a project with pagination + * List all flows for a project with pagination and computed counts + * + * Retrieves flows created automatically when generations/uploads specify: + * - A flowId in their request + * - A flowAlias (creates flow eagerly if doesn't exist) + * + * Each flow includes: + * - Computed generationCount and imageCount + * - Flow-scoped aliases (JSONB key-value pairs) + * - Custom metadata + * + * @route GET /api/v1/flows + * @authentication Project Key required + * + * @param {number} [req.query.limit=20] - Results per page (max 100) + * @param {number} [req.query.offset=0] - Number of results to skip + * + * @returns {ListFlowsResponse} 200 - Paginated list of flows with counts + * @returns {object} 400 - Invalid pagination parameters + * @returns {object} 401 - Missing or invalid API key + * + * @example + * GET /api/v1/flows?limit=50&offset=0 */ flowsRouter.get( '/', @@ -108,8 +129,28 @@ flowsRouter.get( ); /** - * GET /api/v1/flows/:id - * Get a single flow by ID with computed counts + * Get a single flow by ID with computed statistics + * + * Retrieves detailed flow information including: + * - All flow-scoped aliases + * - Computed generationCount (active generations only) + * - Computed imageCount (active images only) + * - Custom metadata + * - Creation and update timestamps + * + * @route GET /api/v1/flows/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Flow ID (UUID) + * + * @returns {GetFlowResponse} 200 - Complete flow details with counts + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} FLOW_NOT_FOUND - Flow does not exist + * + * @example + * GET /api/v1/flows/550e8400-e29b-41d4-a716-446655440000 */ flowsRouter.get( '/:id', @@ -140,8 +181,25 @@ flowsRouter.get( ); /** - * GET /api/v1/flows/:id/generations - * List all generations in a flow with pagination + * List all generations in a specific flow with pagination + * + * Retrieves all generations associated with this flow, ordered by creation date (newest first). + * Includes only active (non-deleted) generations. + * + * @route GET /api/v1/flows/:id/generations + * @authentication Project Key required + * + * @param {string} req.params.id - Flow ID (UUID) + * @param {number} [req.query.limit=20] - Results per page (max 100) + * @param {number} [req.query.offset=0] - Number of results to skip + * + * @returns {ListFlowGenerationsResponse} 200 - Paginated list of generations + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 400 - Invalid pagination parameters + * @returns {object} 401 - Missing or invalid API key + * + * @example + * GET /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/generations?limit=10 */ flowsRouter.get( '/:id/generations', @@ -194,8 +252,25 @@ flowsRouter.get( ); /** - * GET /api/v1/flows/:id/images - * List all images in a flow with pagination + * List all images in a specific flow with pagination + * + * Retrieves all images (generated and uploaded) associated with this flow, + * ordered by creation date (newest first). Includes only active (non-deleted) images. + * + * @route GET /api/v1/flows/:id/images + * @authentication Project Key required + * + * @param {string} req.params.id - Flow ID (UUID) + * @param {number} [req.query.limit=20] - Results per page (max 100) + * @param {number} [req.query.offset=0] - Number of results to skip + * + * @returns {ListFlowImagesResponse} 200 - Paginated list of images + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 400 - Invalid pagination parameters + * @returns {object} 401 - Missing or invalid API key + * + * @example + * GET /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/images?limit=20 */ flowsRouter.get( '/:id/images', @@ -248,8 +323,40 @@ flowsRouter.get( ); /** - * PUT /api/v1/flows/:id/aliases - * Update aliases in a flow (add or update existing aliases) + * Update flow-scoped aliases (add or modify existing) + * + * Updates the JSONB aliases field with new or modified key-value pairs. + * Aliases are merged with existing aliases (does not replace all). + * + * Flow-scoped aliases: + * - Must start with @ symbol + * - Unique within the flow only (not project-wide) + * - Used for alias resolution in generations + * - Stored as JSONB for efficient lookups + * + * @route PUT /api/v1/flows/:id/aliases + * @authentication Project Key required + * + * @param {string} req.params.id - Flow ID (UUID) + * @param {UpdateFlowAliasesRequest} req.body - Alias updates + * @param {object} req.body.aliases - Key-value pairs of aliases to add/update + * + * @returns {UpdateFlowAliasesResponse} 200 - Updated flow with merged aliases + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 400 - Invalid aliases format + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} FLOW_NOT_FOUND - Flow does not exist + * @throws {Error} VALIDATION_ERROR - Aliases must be an object + * + * @example + * PUT /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/aliases + * { + * "aliases": { + * "@hero": "image-id-123", + * "@background": "image-id-456" + * } + * } */ flowsRouter.put( '/:id/aliases', @@ -304,8 +411,25 @@ flowsRouter.put( ); /** - * DELETE /api/v1/flows/:id/aliases/:alias * Remove a specific alias from a flow + * + * Deletes a single alias key-value pair from the flow's JSONB aliases field. + * Other aliases remain unchanged. + * + * @route DELETE /api/v1/flows/:id/aliases/:alias + * @authentication Project Key required + * + * @param {string} req.params.id - Flow ID (UUID) + * @param {string} req.params.alias - Alias to remove (e.g., "@hero") + * + * @returns {object} 200 - Updated flow with alias removed + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} FLOW_NOT_FOUND - Flow does not exist + * + * @example + * DELETE /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/aliases/@hero */ flowsRouter.delete( '/:id/aliases/:alias', @@ -348,10 +472,31 @@ flowsRouter.delete( ); /** - * POST /api/v1/flows/:id/regenerate - * Regenerate the most recent generation in a flow (Section 3.6) + * Regenerate the most recent generation in a flow + * + * Identifies the latest generation in the flow and regenerates it: + * - Uses exact same parameters (prompt, aspect ratio, references) + * - Replaces existing output image (preserves ID and URLs) * - Returns error if flow has no generations - * - Uses parameters from the last generation + * - Ordered by creation date (newest first) + * + * @route POST /api/v1/flows/:id/regenerate + * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.id - Flow ID (UUID) + * + * @returns {object} 200 - Regenerated generation response + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 400 - Flow has no generations + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} FLOW_NOT_FOUND - Flow does not exist + * @throws {Error} FLOW_HAS_NO_GENERATIONS - Flow contains no generations + * + * @example + * POST /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/regenerate */ flowsRouter.post( '/:id/regenerate', @@ -413,8 +558,35 @@ flowsRouter.post( ); /** - * DELETE /api/v1/flows/:id - * Delete a flow + * Delete a flow (hard delete) + * + * Permanently removes the flow record: + * - Flow record is hard deleted (no soft delete) + * - Generations remain intact (not cascaded) + * - Images remain intact (not cascaded) + * - Flow-scoped aliases are removed with flow + * + * Note: Generations and images lose their flow association but remain accessible. + * + * @route DELETE /api/v1/flows/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Flow ID (UUID) + * + * @returns {object} 200 - Deletion confirmation with flow ID + * @returns {object} 404 - Flow not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} FLOW_NOT_FOUND - Flow does not exist + * + * @example + * DELETE /api/v1/flows/550e8400-e29b-41d4-a716-446655440000 + * + * Response: + * { + * "success": true, + * "data": { "id": "550e8400-e29b-41d4-a716-446655440000" } + * } */ flowsRouter.delete( '/:id', diff --git a/apps/api-service/src/routes/v1/generations.ts b/apps/api-service/src/routes/v1/generations.ts index 8eff255..08c2058 100644 --- a/apps/api-service/src/routes/v1/generations.ts +++ b/apps/api-service/src/routes/v1/generations.ts @@ -26,8 +26,56 @@ const getGenerationService = (): GenerationService => { }; /** + * Create a new image generation from a text prompt + * + * Generates AI-powered images using Gemini Flash Image model with support for: + * - Text prompts with optional auto-enhancement + * - Reference images for style/context + * - Flow association and flow-scoped aliases + * - Project-scoped aliases for direct access + * - Custom metadata storage + * + * @route POST /api/v1/generations + * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {CreateGenerationRequest} req.body - Generation parameters + * @param {string} req.body.prompt - Text description of desired image (required) + * @param {string[]} [req.body.referenceImages] - Array of aliases to use as references + * @param {string} [req.body.aspectRatio='1:1'] - Aspect ratio (1:1, 16:9, 3:2, 9:16) + * @param {string} [req.body.flowId] - Associate with existing flow + * @param {string} [req.body.alias] - Project-scoped alias (@custom-name) + * @param {string} [req.body.flowAlias] - Flow-scoped alias (requires flowId) + * @param {boolean} [req.body.autoEnhance=false] - Enable prompt enhancement + * @param {object} [req.body.meta] - Custom metadata + * + * @returns {CreateGenerationResponse} 201 - Generation created with status + * @returns {object} 400 - Invalid request parameters + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} VALIDATION_ERROR - Missing or invalid prompt + * @throws {Error} ALIAS_CONFLICT - Alias already exists + * @throws {Error} FLOW_NOT_FOUND - Flow ID does not exist + * @throws {Error} IMAGE_NOT_FOUND - Reference image alias not found + * + * @example + * // Basic generation * POST /api/v1/generations - * Create a new image generation with optional reference images and aliases + * { + * "prompt": "A serene mountain landscape at sunset", + * "aspectRatio": "16:9" + * } + * + * @example + * // With reference images and alias + * POST /api/v1/generations + * { + * "prompt": "Product photo in this style", + * "referenceImages": ["@brand-style", "@product-template"], + * "alias": "@hero-image", + * "autoEnhance": true + * } */ generationsRouter.post( '/', @@ -83,8 +131,34 @@ generationsRouter.post( ); /** - * GET /api/v1/generations - * List generations with filters and pagination + * List all generations for the project with filtering and pagination + * + * Retrieves generations with support for: + * - Flow-based filtering + * - Status filtering (pending, processing, success, failed) + * - Pagination with configurable limit and offset + * - Optional inclusion of soft-deleted generations + * + * @route GET /api/v1/generations + * @authentication Project Key required + * + * @param {string} [req.query.flowId] - Filter by flow ID + * @param {string} [req.query.status] - Filter by status (pending|processing|success|failed) + * @param {number} [req.query.limit=20] - Results per page (max 100) + * @param {number} [req.query.offset=0] - Number of results to skip + * @param {boolean} [req.query.includeDeleted=false] - Include soft-deleted generations + * + * @returns {ListGenerationsResponse} 200 - Paginated list of generations + * @returns {object} 400 - Invalid pagination parameters + * @returns {object} 401 - Missing or invalid API key + * + * @example + * // List recent generations + * GET /api/v1/generations?limit=10&offset=0 + * + * @example + * // Filter by flow and status + * GET /api/v1/generations?flowId=abc-123&status=success&limit=50 */ generationsRouter.get( '/', @@ -127,8 +201,28 @@ generationsRouter.get( ); /** - * GET /api/v1/generations/:id * Get a single generation by ID with full details + * + * Retrieves complete generation information including: + * - Generation status and metadata + * - Output image details (URL, dimensions, etc.) + * - Reference images used + * - Flow association + * - Timestamps and audit trail + * + * @route GET /api/v1/generations/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Generation ID (UUID) + * + * @returns {GetGenerationResponse} 200 - Complete generation details + * @returns {object} 404 - Generation not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} GENERATION_NOT_FOUND - Generation does not exist + * + * @example + * GET /api/v1/generations/550e8400-e29b-41d4-a716-446655440000 */ generationsRouter.get( '/:id', @@ -159,9 +253,46 @@ generationsRouter.get( ); /** - * PUT /api/v1/generations/:id - * Update generation parameters (prompt, aspectRatio, flowId, meta) - * Generative parameters (prompt, aspectRatio) trigger automatic regeneration + * Update generation parameters with automatic regeneration + * + * Updates generation settings with intelligent regeneration behavior: + * - Changing prompt or aspectRatio triggers automatic regeneration + * - Changing flowId or meta updates metadata only (no regeneration) + * - Regeneration replaces existing output image (same ID and URLs) + * - All changes preserve generation history and IDs + * + * @route PUT /api/v1/generations/:id + * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.id - Generation ID (UUID) + * @param {UpdateGenerationRequest} req.body - Update parameters + * @param {string} [req.body.prompt] - New prompt (triggers regeneration) + * @param {string} [req.body.aspectRatio] - New aspect ratio (triggers regeneration) + * @param {string|null} [req.body.flowId] - Change flow association (null to detach) + * @param {object} [req.body.meta] - Update custom metadata + * + * @returns {GetGenerationResponse} 200 - Updated generation with new output + * @returns {object} 404 - Generation not found or access denied + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} GENERATION_NOT_FOUND - Generation does not exist + * @throws {Error} FLOW_NOT_FOUND - New flow ID does not exist + * + * @example + * // Update prompt (triggers regeneration) + * PUT /api/v1/generations/550e8400-e29b-41d4-a716-446655440000 + * { + * "prompt": "Updated: A mountain landscape with vibrant colors" + * } + * + * @example + * // Change flow association (no regeneration) + * PUT /api/v1/generations/550e8400-e29b-41d4-a716-446655440000 + * { + * "flowId": "new-flow-id-123" + * } */ generationsRouter.put( '/:id', @@ -211,11 +342,30 @@ generationsRouter.put( ); /** - * POST /api/v1/generations/:id/regenerate - * Regenerate existing generation with exact same parameters (Section 3) - * - Allows regeneration for any status - * - Updates existing image (same ID, path, URL) - * - No parameter overrides + * Regenerate existing generation with exact same parameters + * + * Creates a new image using the original generation parameters: + * - Uses exact same prompt, aspect ratio, and reference images + * - Works regardless of current status (success, failed, pending) + * - Replaces existing output image (preserves ID and URLs) + * - No parameter modifications allowed (use PUT for changes) + * - Useful for refreshing stale images or recovering from failures + * + * @route POST /api/v1/generations/:id/regenerate + * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.id - Generation ID (UUID) + * + * @returns {GetGenerationResponse} 200 - Regenerated generation with new output + * @returns {object} 404 - Generation not found or access denied + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} GENERATION_NOT_FOUND - Generation does not exist + * + * @example + * POST /api/v1/generations/550e8400-e29b-41d4-a716-446655440000/regenerate */ generationsRouter.post( '/:id/regenerate', @@ -259,9 +409,25 @@ generationsRouter.post( ); /** - * POST /api/v1/generations/:id/retry - * Legacy endpoint - delegates to regenerate - * @deprecated Use /regenerate instead + * Retry a failed generation (legacy endpoint) + * + * @deprecated Use POST /api/v1/generations/:id/regenerate instead + * + * This endpoint is maintained for backward compatibility and delegates + * to the regenerate endpoint. New integrations should use /regenerate. + * + * @route POST /api/v1/generations/:id/retry + * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.id - Generation ID (UUID) + * + * @returns {CreateGenerationResponse} 201 - Regenerated generation + * @returns {object} 404 - Generation not found or access denied + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @see POST /api/v1/generations/:id/regenerate - Preferred endpoint */ generationsRouter.post( '/:id/retry', @@ -305,8 +471,33 @@ generationsRouter.post( ); /** - * DELETE /api/v1/generations/:id * Delete a generation and its output image + * + * Performs deletion with alias protection: + * - Soft delete generation record (sets deletedAt timestamp) + * - Hard delete output image if no project/flow aliases exist + * - Soft delete output image if aliases exist (preserves for CDN access) + * - Cascades to remove generation-image relationships + * + * @route DELETE /api/v1/generations/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Generation ID (UUID) + * + * @returns {object} 200 - Deletion confirmation with generation ID + * @returns {object} 404 - Generation not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} GENERATION_NOT_FOUND - Generation does not exist + * + * @example + * DELETE /api/v1/generations/550e8400-e29b-41d4-a716-446655440000 + * + * Response: + * { + * "success": true, + * "data": { "id": "550e8400-e29b-41d4-a716-446655440000" } + * } */ generationsRouter.delete( '/:id', diff --git a/apps/api-service/src/routes/v1/images.ts b/apps/api-service/src/routes/v1/images.ts index 306ad44..b1ee8ba 100644 --- a/apps/api-service/src/routes/v1/images.ts +++ b/apps/api-service/src/routes/v1/images.ts @@ -43,8 +43,52 @@ const getAliasService = (): AliasService => { }; /** + * Upload a single image file to project storage + * + * Uploads an image file to MinIO storage and creates a database record with support for: + * - Automatic flow creation when flowId is undefined (lazy creation) + * - Eager flow creation when flowAlias is provided + * - Project-scoped alias assignment + * - Custom metadata storage + * - Multiple file formats (JPEG, PNG, WebP, etc.) + * + * FlowId behavior: + * - undefined (not provided) → generates new UUID for automatic flow creation + * - null (explicitly null) → no flow association + * - string (specific value) → uses provided flow ID + * + * @route POST /api/v1/images/upload + * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {File} req.file - Image file (multipart/form-data, max 5MB) + * @param {string} [req.body.alias] - Project-scoped alias (@custom-name) + * @param {string|null} [req.body.flowId] - Flow association (undefined=auto, null=none, string=specific) + * @param {string} [req.body.flowAlias] - Flow-scoped alias (requires flowId, triggers eager creation) + * @param {string} [req.body.meta] - Custom metadata (JSON string) + * + * @returns {UploadImageResponse} 201 - Uploaded image with storage details + * @returns {object} 400 - Missing file or validation error + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 413 - File too large + * @returns {object} 415 - Unsupported file type + * @returns {object} 429 - Rate limit exceeded + * @returns {object} 500 - Upload or storage error + * + * @throws {Error} VALIDATION_ERROR - No file provided + * @throws {Error} UPLOAD_ERROR - File upload failed + * @throws {Error} ALIAS_CONFLICT - Alias already exists + * + * @example + * // Upload with automatic flow creation * POST /api/v1/images/upload - * Upload a single image file and create database record + * Content-Type: multipart/form-data + * { file: , alias: "@hero-bg" } + * + * @example + * // Upload with eager flow creation and flow alias + * POST /api/v1/images/upload + * { file: , flowAlias: "@step-1" } */ imagesRouter.post( '/upload', @@ -177,8 +221,32 @@ imagesRouter.post( ); /** - * GET /api/v1/images - * List images with filters and pagination + * List all images for the project with filtering and pagination + * + * Retrieves images (both generated and uploaded) with support for: + * - Flow-based filtering + * - Source filtering (generated vs uploaded) + * - Alias filtering (exact match) + * - Pagination with configurable limit and offset + * - Optional inclusion of soft-deleted images + * + * @route GET /api/v1/images + * @authentication Project Key required + * + * @param {string} [req.query.flowId] - Filter by flow ID + * @param {string} [req.query.source] - Filter by source (generated|uploaded) + * @param {string} [req.query.alias] - Filter by exact alias match + * @param {number} [req.query.limit=20] - Results per page (max 100) + * @param {number} [req.query.offset=0] - Number of results to skip + * @param {boolean} [req.query.includeDeleted=false] - Include soft-deleted images + * + * @returns {ListImagesResponse} 200 - Paginated list of images + * @returns {object} 400 - Invalid pagination parameters + * @returns {object} 401 - Missing or invalid API key + * + * @example + * // List uploaded images in a flow + * GET /api/v1/images?flowId=abc-123&source=uploaded&limit=50 */ imagesRouter.get( '/', @@ -222,8 +290,39 @@ imagesRouter.get( ); /** - * GET /api/v1/images/resolve/:alias - * Resolve an alias to an image using 3-tier precedence (technical -> flow -> project) + * Resolve an alias to an image using 3-tier precedence system + * + * Resolves aliases through a priority-based lookup system: + * 1. Technical aliases (@last, @first, @upload) - computed on-the-fly + * 2. Flow-scoped aliases - looked up in flow's JSONB aliases field (requires flowId) + * 3. Project-scoped aliases - looked up in images.alias column + * + * Returns the image ID, resolution scope, and complete image details. + * + * @route GET /api/v1/images/resolve/:alias + * @authentication Project Key required + * + * @param {string} req.params.alias - Alias to resolve (e.g., "@last", "@hero", "@step-1") + * @param {string} [req.query.flowId] - Flow context for flow-scoped resolution + * + * @returns {ResolveAliasResponse} 200 - Resolved image with scope and details + * @returns {object} 404 - Alias not found in any scope + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} ALIAS_NOT_FOUND - Alias does not exist + * @throws {Error} RESOLUTION_ERROR - Resolution failed + * + * @example + * // Resolve technical alias + * GET /api/v1/images/resolve/@last + * + * @example + * // Resolve flow-scoped alias + * GET /api/v1/images/resolve/@step-1?flowId=abc-123 + * + * @example + * // Resolve project-scoped alias + * GET /api/v1/images/resolve/@hero-bg */ imagesRouter.get( '/resolve/:alias', @@ -290,8 +389,29 @@ imagesRouter.get( ); /** - * GET /api/v1/images/:id - * Get a single image by ID + * Get a single image by ID with complete details + * + * Retrieves full image information including: + * - Storage URLs and keys + * - Project and flow associations + * - Alias assignments (project-scoped) + * - Source (generated vs uploaded) + * - File metadata (size, MIME type, hash) + * - Focal point and custom metadata + * + * @route GET /api/v1/images/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Image ID (UUID) + * + * @returns {GetImageResponse} 200 - Complete image details + * @returns {object} 404 - Image not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} IMAGE_NOT_FOUND - Image does not exist + * + * @example + * GET /api/v1/images/550e8400-e29b-41d4-a716-446655440000 */ imagesRouter.get( '/:id', @@ -332,8 +452,36 @@ imagesRouter.get( ); /** - * PUT /api/v1/images/:id - * Update image metadata (alias, focal point, meta) + * Update image metadata (focal point and custom metadata) + * + * Updates non-generative image properties: + * - Focal point for image cropping (x, y coordinates 0.0-1.0) + * - Custom metadata (arbitrary JSON object) + * + * Note: Alias assignment moved to separate endpoint PUT /images/:id/alias (Section 6.1) + * + * @route PUT /api/v1/images/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Image ID (UUID) + * @param {UpdateImageRequest} req.body - Update parameters + * @param {object} [req.body.focalPoint] - Focal point for cropping + * @param {number} req.body.focalPoint.x - X coordinate (0.0-1.0) + * @param {number} req.body.focalPoint.y - Y coordinate (0.0-1.0) + * @param {object} [req.body.meta] - Custom metadata + * + * @returns {UpdateImageResponse} 200 - Updated image details + * @returns {object} 404 - Image not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} IMAGE_NOT_FOUND - Image does not exist + * + * @example + * PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000 + * { + * "focalPoint": { "x": 0.5, "y": 0.3 }, + * "meta": { "category": "hero", "priority": 1 } + * } */ imagesRouter.put( '/:id', @@ -385,8 +533,39 @@ imagesRouter.put( ); /** - * PUT /api/v1/images/:id/alias * Assign a project-scoped alias to an image + * + * Sets or updates the project-scoped alias for an image: + * - Alias must start with @ symbol + * - Must be unique within the project + * - Replaces existing alias if image already has one + * - Used for alias resolution in generations and CDN access + * + * This is a dedicated endpoint introduced in Section 6.1 to separate + * alias assignment from general metadata updates. + * + * @route PUT /api/v1/images/:id/alias + * @authentication Project Key required + * + * @param {string} req.params.id - Image ID (UUID) + * @param {object} req.body - Request body + * @param {string} req.body.alias - Project-scoped alias (e.g., "@hero-bg") + * + * @returns {UpdateImageResponse} 200 - Updated image with new alias + * @returns {object} 404 - Image not found or access denied + * @returns {object} 400 - Missing or invalid alias + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 409 - Alias already exists + * + * @throws {Error} IMAGE_NOT_FOUND - Image does not exist + * @throws {Error} VALIDATION_ERROR - Alias is required + * @throws {Error} ALIAS_CONFLICT - Alias already assigned to another image + * + * @example + * PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias + * { + * "alias": "@hero-background" + * } */ imagesRouter.put( '/:id/alias', @@ -441,8 +620,37 @@ imagesRouter.put( ); /** - * DELETE /api/v1/images/:id - * Hard delete an image with MinIO cleanup and cascades (Section 7.1) + * Delete an image with storage cleanup and cascading deletions + * + * Performs hard delete of image record and MinIO file with cascading operations: + * - Deletes image record from database (hard delete, no soft delete) + * - Removes file from MinIO storage permanently + * - Cascades to delete generation-image relationships + * - Removes image from flow aliases (if present) + * - Cannot be undone + * + * Use with caution: This is a destructive operation that permanently removes + * the image file and all database references. + * + * @route DELETE /api/v1/images/:id + * @authentication Project Key required + * + * @param {string} req.params.id - Image ID (UUID) + * + * @returns {DeleteImageResponse} 200 - Deletion confirmation with image ID + * @returns {object} 404 - Image not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} IMAGE_NOT_FOUND - Image does not exist + * + * @example + * DELETE /api/v1/images/550e8400-e29b-41d4-a716-446655440000 + * + * Response: + * { + * "success": true, + * "data": { "id": "550e8400-e29b-41d4-a716-446655440000" } + * } */ imagesRouter.delete( '/:id', diff --git a/apps/api-service/src/routes/v1/scopes.ts b/apps/api-service/src/routes/v1/scopes.ts index 4f4d79b..7f442a0 100644 --- a/apps/api-service/src/routes/v1/scopes.ts +++ b/apps/api-service/src/routes/v1/scopes.ts @@ -51,9 +51,44 @@ const getGenerationService = (): GenerationService => { }; /** - * POST /api/v1/live/scopes - * Create new live scope manually (Section 8.5) + * Create a new live scope manually with settings + * + * Creates a live scope for organizing live URL generations: + * - Slug must be unique within the project + * - Slug format: alphanumeric + hyphens + underscores only + * - Configure generation limits and permissions + * - Optional custom metadata storage + * + * Note: Scopes are typically auto-created via live URLs, but this endpoint + * allows pre-configuration with specific settings. + * + * @route POST /api/v1/live/scopes * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {CreateLiveScopeRequest} req.body - Scope configuration + * @param {string} req.body.slug - Unique scope identifier (alphanumeric + hyphens + underscores) + * @param {boolean} [req.body.allowNewGenerations=true] - Allow new generations in scope + * @param {number} [req.body.newGenerationsLimit=30] - Maximum generations allowed + * @param {object} [req.body.meta] - Custom metadata + * + * @returns {CreateLiveScopeResponse} 201 - Created scope with stats + * @returns {object} 400 - Invalid slug format + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 409 - Scope slug already exists + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} SCOPE_INVALID_FORMAT - Invalid slug format + * @throws {Error} SCOPE_ALREADY_EXISTS - Slug already in use + * + * @example + * POST /api/v1/live/scopes + * { + * "slug": "hero-section", + * "allowNewGenerations": true, + * "newGenerationsLimit": 50, + * "meta": { "description": "Hero section images" } + * } */ scopesRouter.post( '/', @@ -110,9 +145,27 @@ scopesRouter.post( ); /** - * GET /api/v1/live/scopes - * List all live scopes for a project (Section 8.5) + * List all live scopes for the project with pagination and statistics + * + * Retrieves all scopes (both auto-created and manually created) with: + * - Computed currentGenerations count (active only) + * - Last generation timestamp + * - Pagination support + * - Optional slug filtering + * + * @route GET /api/v1/live/scopes * @authentication Project Key required + * + * @param {string} [req.query.slug] - Filter by exact slug match + * @param {number} [req.query.limit=20] - Results per page (max 100) + * @param {number} [req.query.offset=0] - Number of results to skip + * + * @returns {ListLiveScopesResponse} 200 - Paginated list of scopes with stats + * @returns {object} 400 - Invalid pagination parameters + * @returns {object} 401 - Missing or invalid API key + * + * @example + * GET /api/v1/live/scopes?limit=50&offset=0 */ scopesRouter.get( '/', @@ -146,9 +199,28 @@ scopesRouter.get( ); /** - * GET /api/v1/live/scopes/:slug - * Get single live scope by slug (Section 8.5) + * Get a single live scope by slug with complete statistics + * + * Retrieves detailed scope information including: + * - Current generation count (active generations only) + * - Last generation timestamp + * - Settings (allowNewGenerations, newGenerationsLimit) + * - Custom metadata + * - Creation and update timestamps + * + * @route GET /api/v1/live/scopes/:slug * @authentication Project Key required + * + * @param {string} req.params.slug - Scope slug identifier + * + * @returns {GetLiveScopeResponse} 200 - Complete scope details with stats + * @returns {object} 404 - Scope not found or access denied + * @returns {object} 401 - Missing or invalid API key + * + * @throws {Error} SCOPE_NOT_FOUND - Scope does not exist + * + * @example + * GET /api/v1/live/scopes/hero-section */ scopesRouter.get( '/:slug', @@ -169,9 +241,38 @@ scopesRouter.get( ); /** - * PUT /api/v1/live/scopes/:slug - * Update live scope settings (Section 8.5) + * Update live scope settings and metadata + * + * Modifies scope configuration: + * - Enable/disable new generations + * - Adjust generation limits + * - Update custom metadata + * + * Changes take effect immediately for new live URL requests. + * + * @route PUT /api/v1/live/scopes/:slug * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.slug - Scope slug identifier + * @param {UpdateLiveScopeRequest} req.body - Update parameters + * @param {boolean} [req.body.allowNewGenerations] - Allow/disallow new generations + * @param {number} [req.body.newGenerationsLimit] - Update generation limit + * @param {object} [req.body.meta] - Update custom metadata + * + * @returns {UpdateLiveScopeResponse} 200 - Updated scope with stats + * @returns {object} 404 - Scope not found or access denied + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} SCOPE_NOT_FOUND - Scope does not exist + * + * @example + * PUT /api/v1/live/scopes/hero-section + * { + * "allowNewGenerations": false, + * "newGenerationsLimit": 100 + * } */ scopesRouter.put( '/:slug', @@ -210,9 +311,46 @@ scopesRouter.put( ); /** - * POST /api/v1/live/scopes/:slug/regenerate - * Regenerate images in scope (Section 8.5) + * Regenerate images in a live scope + * + * Regenerates either a specific image or all images in the scope: + * - Specific image: Provide imageId in request body + * - All images: Omit imageId to regenerate entire scope + * - Uses exact same parameters (prompt, aspect ratio, etc.) + * - Updates existing images (preserves IDs and URLs) + * - Verifies image belongs to scope before regenerating + * + * Useful for refreshing stale cached images or recovering from failures. + * + * @route POST /api/v1/live/scopes/:slug/regenerate * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.slug - Scope slug identifier + * @param {RegenerateScopeRequest} [req.body] - Regeneration options + * @param {string} [req.body.imageId] - Specific image to regenerate (omit for all) + * + * @returns {RegenerateScopeResponse} 200 - Regeneration results + * @returns {object} 400 - Image not in scope + * @returns {object} 404 - Scope or image not found + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} SCOPE_NOT_FOUND - Scope does not exist + * @throws {Error} IMAGE_NOT_FOUND - Image does not exist + * @throws {Error} IMAGE_NOT_IN_SCOPE - Image doesn't belong to scope + * + * @example + * // Regenerate specific image + * POST /api/v1/live/scopes/hero-section/regenerate + * { + * "imageId": "550e8400-e29b-41d4-a716-446655440000" + * } + * + * @example + * // Regenerate all images in scope + * POST /api/v1/live/scopes/hero-section/regenerate + * {} */ scopesRouter.post( '/:slug/regenerate', @@ -307,10 +445,38 @@ scopesRouter.post( ); /** - * DELETE /api/v1/live/scopes/:slug - * Delete live scope (Section 8.5) - * Deletes all images in scope following standard deletion rules + * Delete a live scope with cascading image deletion + * + * Permanently removes the scope and all its associated images: + * - Hard deletes all images in scope (MinIO + database) + * - Follows alias protection rules for each image + * - Hard deletes scope record (no soft delete) + * - Cannot be undone + * + * Use with caution: This is a destructive operation that permanently + * removes the scope and all cached live URL images. + * + * @route DELETE /api/v1/live/scopes/:slug * @authentication Project Key required + * @rateLimit 100 requests per hour per API key + * + * @param {string} req.params.slug - Scope slug identifier + * + * @returns {DeleteLiveScopeResponse} 200 - Deletion confirmation with scope ID + * @returns {object} 404 - Scope not found or access denied + * @returns {object} 401 - Missing or invalid API key + * @returns {object} 429 - Rate limit exceeded + * + * @throws {Error} SCOPE_NOT_FOUND - Scope does not exist + * + * @example + * DELETE /api/v1/live/scopes/hero-section + * + * Response: + * { + * "success": true, + * "data": { "id": "550e8400-e29b-41d4-a716-446655440000" } + * } */ scopesRouter.delete( '/:slug',