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:
parent
fa65264410
commit
a92b1bf482
|
|
@ -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 CDN endpoint for serving images without authentication:
|
||||||
* Public endpoint - no authentication required
|
* - Supports filename-based access (exact match in storageKey)
|
||||||
* Returns image bytes with caching headers
|
* - 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(
|
cdnRouter.get(
|
||||||
'/:orgSlug/:projectSlug/img/:filenameOrAlias',
|
'/: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 for on-demand image generation via URL parameters:
|
||||||
* Public endpoint - no authentication required
|
* - No authentication required (public access)
|
||||||
* Query params: prompt, aspectRatio, autoEnhance, template
|
* - 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:
|
* Cache behavior:
|
||||||
* 1. Resolve org, project, and scope
|
* - Cache HIT: Returns existing image instantly, no rate limit check
|
||||||
* 2. Compute cache key from params
|
* - Cache MISS: Generates new image, counts toward IP rate limit
|
||||||
* 3. Check if image exists in cache (via meta field)
|
* - Cache key computed from: prompt + aspectRatio + autoEnhance + template
|
||||||
* 4. If HIT: return cached image
|
* - Cached images stored with meta.isLiveUrl = true
|
||||||
* 5. If MISS: check scope limits, generate new image, cache, return
|
*
|
||||||
|
* 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(
|
cdnRouter.get(
|
||||||
'/:orgSlug/:projectSlug/live/:scope',
|
'/:orgSlug/:projectSlug/live/:scope',
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,29 @@ const getGenerationService = (): GenerationService => {
|
||||||
// );
|
// );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/flows
|
* List all flows for a project with pagination and computed counts
|
||||||
* List all flows for a project with pagination
|
*
|
||||||
|
* 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(
|
flowsRouter.get(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -108,8 +129,28 @@ flowsRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/flows/:id
|
* Get a single flow by ID with computed statistics
|
||||||
* Get a single flow by ID with computed counts
|
*
|
||||||
|
* 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(
|
flowsRouter.get(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
@ -140,8 +181,25 @@ flowsRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/flows/:id/generations
|
* List all generations in a specific flow with pagination
|
||||||
* List all generations in a 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(
|
flowsRouter.get(
|
||||||
'/:id/generations',
|
'/:id/generations',
|
||||||
|
|
@ -194,8 +252,25 @@ flowsRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/flows/:id/images
|
* List all images in a specific flow with pagination
|
||||||
* List all images in a 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(
|
flowsRouter.get(
|
||||||
'/:id/images',
|
'/:id/images',
|
||||||
|
|
@ -248,8 +323,40 @@ flowsRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/v1/flows/:id/aliases
|
* Update flow-scoped aliases (add or modify existing)
|
||||||
* Update aliases in a flow (add or update existing aliases)
|
*
|
||||||
|
* 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(
|
flowsRouter.put(
|
||||||
'/:id/aliases',
|
'/:id/aliases',
|
||||||
|
|
@ -304,8 +411,25 @@ flowsRouter.put(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/v1/flows/:id/aliases/:alias
|
|
||||||
* Remove a specific alias from a flow
|
* 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(
|
flowsRouter.delete(
|
||||||
'/:id/aliases/:alias',
|
'/:id/aliases/:alias',
|
||||||
|
|
@ -348,10 +472,31 @@ flowsRouter.delete(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v1/flows/:id/regenerate
|
* Regenerate the most recent generation in a flow
|
||||||
* Regenerate the most recent generation in a flow (Section 3.6)
|
*
|
||||||
|
* 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
|
* - 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(
|
flowsRouter.post(
|
||||||
'/:id/regenerate',
|
'/:id/regenerate',
|
||||||
|
|
@ -413,8 +558,35 @@ flowsRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/v1/flows/:id
|
* Delete a flow (hard delete)
|
||||||
* Delete a flow
|
*
|
||||||
|
* 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(
|
flowsRouter.delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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(
|
generationsRouter.post(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -83,8 +131,34 @@ generationsRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/generations
|
* List all generations for the project with filtering and pagination
|
||||||
* List generations with filters 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(
|
generationsRouter.get(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -127,8 +201,28 @@ generationsRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/generations/:id
|
|
||||||
* Get a single generation by ID with full details
|
* 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(
|
generationsRouter.get(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
@ -159,9 +253,46 @@ generationsRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/v1/generations/:id
|
* Update generation parameters with automatic regeneration
|
||||||
* Update generation parameters (prompt, aspectRatio, flowId, meta)
|
*
|
||||||
* Generative parameters (prompt, aspectRatio) trigger 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(
|
generationsRouter.put(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
@ -211,11 +342,30 @@ generationsRouter.put(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v1/generations/:id/regenerate
|
* Regenerate existing generation with exact same parameters
|
||||||
* Regenerate existing generation with exact same parameters (Section 3)
|
*
|
||||||
* - Allows regeneration for any status
|
* Creates a new image using the original generation parameters:
|
||||||
* - Updates existing image (same ID, path, URL)
|
* - Uses exact same prompt, aspect ratio, and reference images
|
||||||
* - No parameter overrides
|
* - 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(
|
generationsRouter.post(
|
||||||
'/:id/regenerate',
|
'/:id/regenerate',
|
||||||
|
|
@ -259,9 +409,25 @@ generationsRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v1/generations/:id/retry
|
* Retry a failed generation (legacy endpoint)
|
||||||
* Legacy endpoint - delegates to regenerate
|
*
|
||||||
* @deprecated Use /regenerate instead
|
* @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(
|
generationsRouter.post(
|
||||||
'/:id/retry',
|
'/:id/retry',
|
||||||
|
|
@ -305,8 +471,33 @@ generationsRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/v1/generations/:id
|
|
||||||
* Delete a generation and its output image
|
* 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(
|
generationsRouter.delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
|
||||||
|
|
@ -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
|
* 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(
|
imagesRouter.post(
|
||||||
'/upload',
|
'/upload',
|
||||||
|
|
@ -177,8 +221,32 @@ imagesRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/images
|
* List all images for the project with filtering and pagination
|
||||||
* List images with filters 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(
|
imagesRouter.get(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -222,8 +290,39 @@ imagesRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/images/resolve/:alias
|
* Resolve an alias to an image using 3-tier precedence system
|
||||||
* Resolve an alias to an image using 3-tier precedence (technical -> flow -> project)
|
*
|
||||||
|
* 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(
|
imagesRouter.get(
|
||||||
'/resolve/:alias',
|
'/resolve/:alias',
|
||||||
|
|
@ -290,8 +389,29 @@ imagesRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/images/:id
|
* Get a single image by ID with complete details
|
||||||
* Get a single image by ID
|
*
|
||||||
|
* 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(
|
imagesRouter.get(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
@ -332,8 +452,36 @@ imagesRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/v1/images/:id
|
* Update image metadata (focal point and custom metadata)
|
||||||
* Update image metadata (alias, focal point, meta)
|
*
|
||||||
|
* 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(
|
imagesRouter.put(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
@ -385,8 +533,39 @@ imagesRouter.put(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/v1/images/:id/alias
|
|
||||||
* Assign a project-scoped alias to an image
|
* 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(
|
imagesRouter.put(
|
||||||
'/:id/alias',
|
'/:id/alias',
|
||||||
|
|
@ -441,8 +620,37 @@ imagesRouter.put(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/v1/images/:id
|
* Delete an image with storage cleanup and cascading deletions
|
||||||
* Hard delete an image with MinIO cleanup and cascades (Section 7.1)
|
*
|
||||||
|
* 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(
|
imagesRouter.delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,44 @@ const getGenerationService = (): GenerationService => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v1/live/scopes
|
* Create a new live scope manually with settings
|
||||||
* Create new live scope manually (Section 8.5)
|
*
|
||||||
|
* 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
|
* @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(
|
scopesRouter.post(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -110,9 +145,27 @@ scopesRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/live/scopes
|
* List all live scopes for the project with pagination and statistics
|
||||||
* List all live scopes for a project (Section 8.5)
|
*
|
||||||
|
* 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
|
* @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(
|
scopesRouter.get(
|
||||||
'/',
|
'/',
|
||||||
|
|
@ -146,9 +199,28 @@ scopesRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/live/scopes/:slug
|
* Get a single live scope by slug with complete statistics
|
||||||
* Get single live scope by slug (Section 8.5)
|
*
|
||||||
|
* 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
|
* @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(
|
scopesRouter.get(
|
||||||
'/:slug',
|
'/:slug',
|
||||||
|
|
@ -169,9 +241,38 @@ scopesRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/v1/live/scopes/:slug
|
* Update live scope settings and metadata
|
||||||
* Update live scope settings (Section 8.5)
|
*
|
||||||
|
* 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
|
* @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(
|
scopesRouter.put(
|
||||||
'/:slug',
|
'/:slug',
|
||||||
|
|
@ -210,9 +311,46 @@ scopesRouter.put(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v1/live/scopes/:slug/regenerate
|
* Regenerate images in a live scope
|
||||||
* Regenerate images in scope (Section 8.5)
|
*
|
||||||
|
* 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
|
* @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(
|
scopesRouter.post(
|
||||||
'/:slug/regenerate',
|
'/:slug/regenerate',
|
||||||
|
|
@ -307,10 +445,38 @@ scopesRouter.post(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/v1/live/scopes/:slug
|
* Delete a live scope with cascading image deletion
|
||||||
* Delete live scope (Section 8.5)
|
*
|
||||||
* Deletes all images in scope following standard deletion rules
|
* 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
|
* @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(
|
scopesRouter.delete(
|
||||||
'/:slug',
|
'/:slug',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue