docs: phase 3 part 4 - comprehensive JSDoc documentation for all v1 API endpoints

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 <noreply@anthropic.com>
This commit is contained in:
Oleg Proskurin 2025-11-17 23:32:13 +07:00
parent fa65264410
commit a92b1bf482
5 changed files with 908 additions and 71 deletions

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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: <image.jpg>, alias: "@hero-bg" }
*
* @example
* // Upload with eager flow creation and flow alias
* POST /api/v1/images/upload
* { file: <image.jpg>, 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',

View File

@ -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',