fix: aliases
This commit is contained in:
parent
88cb1f2c61
commit
8623442157
|
|
@ -43,6 +43,42 @@ const getAliasService = (): AliasService => {
|
||||||
return aliasService;
|
return aliasService;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve id_or_alias parameter to imageId
|
||||||
|
* Supports both UUID and alias (@-prefixed) identifiers
|
||||||
|
* Per Section 6.2 of api-refactoring-final.md
|
||||||
|
*
|
||||||
|
* @param identifier - UUID or alias string
|
||||||
|
* @param projectId - Project ID for alias resolution
|
||||||
|
* @param flowId - Optional flow ID for flow-scoped alias resolution
|
||||||
|
* @returns imageId (UUID)
|
||||||
|
* @throws Error if alias not found
|
||||||
|
*/
|
||||||
|
async function resolveImageIdentifier(
|
||||||
|
identifier: string,
|
||||||
|
projectId: string,
|
||||||
|
flowId?: string
|
||||||
|
): Promise<string> {
|
||||||
|
// Check if parameter is alias (starts with @)
|
||||||
|
if (identifier.startsWith('@')) {
|
||||||
|
const aliasServiceInstance = getAliasService();
|
||||||
|
const resolution = await aliasServiceInstance.resolve(
|
||||||
|
identifier,
|
||||||
|
projectId,
|
||||||
|
flowId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!resolution) {
|
||||||
|
throw new Error(`Alias '${identifier}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolution.imageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise treat as UUID
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload a single image file to project storage
|
* Upload a single image file to project storage
|
||||||
*
|
*
|
||||||
|
|
@ -206,12 +242,17 @@ imagesRouter.post(
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
fileHash: null,
|
fileHash: null,
|
||||||
source: 'uploaded',
|
source: 'uploaded',
|
||||||
alias: alias || null,
|
alias: null,
|
||||||
meta: meta ? JSON.parse(meta) : {},
|
meta: meta ? JSON.parse(meta) : {},
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reassign project alias if provided (override behavior per Section 5.2)
|
||||||
|
if (alias) {
|
||||||
|
await service.reassignProjectAlias(alias, imageRecord.id, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
// Eager flow creation if flowAlias is provided
|
// Eager flow creation if flowAlias is provided
|
||||||
if (flowAlias) {
|
if (flowAlias) {
|
||||||
// Use pendingFlowId if available, otherwise finalFlowId
|
// Use pendingFlowId if available, otherwise finalFlowId
|
||||||
|
|
@ -344,8 +385,21 @@ imagesRouter.get(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated Use GET /api/v1/images/:alias directly instead (Section 6.2)
|
||||||
|
*
|
||||||
* Resolve an alias to an image using 3-tier precedence system
|
* Resolve an alias to an image using 3-tier precedence system
|
||||||
*
|
*
|
||||||
|
* **DEPRECATED**: This endpoint is deprecated as of Section 6.2. Use the main
|
||||||
|
* GET /api/v1/images/:id_or_alias endpoint instead, which supports both UUIDs
|
||||||
|
* and aliases (@-prefixed) directly in the path parameter.
|
||||||
|
*
|
||||||
|
* **Migration Guide**:
|
||||||
|
* - Old: GET /api/v1/images/resolve/@hero
|
||||||
|
* - New: GET /api/v1/images/@hero
|
||||||
|
*
|
||||||
|
* This endpoint remains functional for backwards compatibility but will be
|
||||||
|
* removed in a future version.
|
||||||
|
*
|
||||||
* Resolves aliases through a priority-based lookup system:
|
* Resolves aliases through a priority-based lookup system:
|
||||||
* 1. Technical aliases (@last, @first, @upload) - computed on-the-fly
|
* 1. Technical aliases (@last, @first, @upload) - computed on-the-fly
|
||||||
* 2. Flow-scoped aliases - looked up in flow's JSONB aliases field (requires flowId)
|
* 2. Flow-scoped aliases - looked up in flow's JSONB aliases field (requires flowId)
|
||||||
|
|
@ -359,7 +413,7 @@ imagesRouter.get(
|
||||||
* @param {string} req.params.alias - Alias to resolve (e.g., "@last", "@hero", "@step-1")
|
* @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
|
* @param {string} [req.query.flowId] - Flow context for flow-scoped resolution
|
||||||
*
|
*
|
||||||
* @returns {ResolveAliasResponse} 200 - Resolved image with scope and details
|
* @returns {ResolveAliasResponse} 200 - Resolved image with scope and details (includes X-Deprecated header)
|
||||||
* @returns {object} 404 - Alias not found in any scope
|
* @returns {object} 404 - Alias not found in any scope
|
||||||
* @returns {object} 401 - Missing or invalid API key
|
* @returns {object} 401 - Missing or invalid API key
|
||||||
*
|
*
|
||||||
|
|
@ -389,6 +443,12 @@ imagesRouter.get(
|
||||||
|
|
||||||
const projectId = req.apiKey.projectId;
|
const projectId = req.apiKey.projectId;
|
||||||
|
|
||||||
|
// Add deprecation header
|
||||||
|
res.setHeader(
|
||||||
|
'X-Deprecated',
|
||||||
|
'This endpoint is deprecated. Use GET /api/v1/images/:alias instead (Section 6.2)'
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resolution = await aliasServiceInstance.resolve(
|
const resolution = await aliasServiceInstance.resolve(
|
||||||
alias,
|
alias,
|
||||||
|
|
@ -453,10 +513,11 @@ imagesRouter.get(
|
||||||
* - File metadata (size, MIME type, hash)
|
* - File metadata (size, MIME type, hash)
|
||||||
* - Focal point and custom metadata
|
* - Focal point and custom metadata
|
||||||
*
|
*
|
||||||
* @route GET /api/v1/images/:id
|
* @route GET /api/v1/images/:id_or_alias
|
||||||
* @authentication Project Key required
|
* @authentication Project Key required
|
||||||
*
|
*
|
||||||
* @param {string} req.params.id - Image ID (UUID)
|
* @param {string} req.params.id_or_alias - Image ID (UUID) or alias (@alias)
|
||||||
|
* @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution
|
||||||
*
|
*
|
||||||
* @returns {GetImageResponse} 200 - Complete image details
|
* @returns {GetImageResponse} 200 - Complete image details
|
||||||
* @returns {object} 404 - Image not found or access denied
|
* @returns {object} 404 - Image not found or access denied
|
||||||
|
|
@ -466,16 +527,38 @@ imagesRouter.get(
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* GET /api/v1/images/550e8400-e29b-41d4-a716-446655440000
|
* GET /api/v1/images/550e8400-e29b-41d4-a716-446655440000
|
||||||
|
* GET /api/v1/images/@hero
|
||||||
|
* GET /api/v1/images/@hero?flowId=abc-123
|
||||||
*/
|
*/
|
||||||
imagesRouter.get(
|
imagesRouter.get(
|
||||||
'/:id',
|
'/:id_or_alias',
|
||||||
validateApiKey,
|
validateApiKey,
|
||||||
requireProjectKey,
|
requireProjectKey,
|
||||||
asyncHandler(async (req: any, res: Response<GetImageResponse>) => {
|
asyncHandler(async (req: any, res: Response<GetImageResponse>) => {
|
||||||
const service = getImageService();
|
const service = getImageService();
|
||||||
const { id } = req.params;
|
const { id_or_alias } = req.params;
|
||||||
|
const { flowId } = req.query;
|
||||||
|
|
||||||
const image = await service.getById(id);
|
// Resolve alias to imageId if needed (Section 6.2)
|
||||||
|
let imageId: string;
|
||||||
|
try {
|
||||||
|
imageId = await resolveImageIdentifier(
|
||||||
|
id_or_alias,
|
||||||
|
req.apiKey.projectId,
|
||||||
|
flowId as string | undefined
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: error instanceof Error ? error.message : 'Image not found',
|
||||||
|
code: 'IMAGE_NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await service.getById(imageId);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -513,11 +596,13 @@ imagesRouter.get(
|
||||||
* - Custom metadata (arbitrary JSON object)
|
* - Custom metadata (arbitrary JSON object)
|
||||||
*
|
*
|
||||||
* Note: Alias assignment moved to separate endpoint PUT /images/:id/alias (Section 6.1)
|
* Note: Alias assignment moved to separate endpoint PUT /images/:id/alias (Section 6.1)
|
||||||
|
* Supports both UUID and alias (@-prefixed) identifiers per Section 6.2.
|
||||||
*
|
*
|
||||||
* @route PUT /api/v1/images/:id
|
* @route PUT /api/v1/images/:id_or_alias
|
||||||
* @authentication Project Key required
|
* @authentication Project Key required
|
||||||
*
|
*
|
||||||
* @param {string} req.params.id - Image ID (UUID)
|
* @param {string} req.params.id_or_alias - Image ID (UUID) or alias (@-prefixed)
|
||||||
|
* @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution
|
||||||
* @param {UpdateImageRequest} req.body - Update parameters
|
* @param {UpdateImageRequest} req.body - Update parameters
|
||||||
* @param {object} [req.body.focalPoint] - Focal point for cropping
|
* @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.x - X coordinate (0.0-1.0)
|
||||||
|
|
@ -530,23 +615,55 @@ imagesRouter.get(
|
||||||
*
|
*
|
||||||
* @throws {Error} IMAGE_NOT_FOUND - Image does not exist
|
* @throws {Error} IMAGE_NOT_FOUND - Image does not exist
|
||||||
*
|
*
|
||||||
* @example
|
* @example UUID identifier
|
||||||
* PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000
|
* PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000
|
||||||
* {
|
* {
|
||||||
* "focalPoint": { "x": 0.5, "y": 0.3 },
|
* "focalPoint": { "x": 0.5, "y": 0.3 },
|
||||||
* "meta": { "category": "hero", "priority": 1 }
|
* "meta": { "category": "hero", "priority": 1 }
|
||||||
* }
|
* }
|
||||||
|
*
|
||||||
|
* @example Project-scoped alias
|
||||||
|
* PUT /api/v1/images/@hero-banner
|
||||||
|
* {
|
||||||
|
* "focalPoint": { "x": 0.5, "y": 0.3 }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example Flow-scoped alias
|
||||||
|
* PUT /api/v1/images/@product-shot?flowId=123e4567-e89b-12d3-a456-426614174000
|
||||||
|
* {
|
||||||
|
* "meta": { "category": "product" }
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
imagesRouter.put(
|
imagesRouter.put(
|
||||||
'/:id',
|
'/:id_or_alias',
|
||||||
validateApiKey,
|
validateApiKey,
|
||||||
requireProjectKey,
|
requireProjectKey,
|
||||||
asyncHandler(async (req: any, res: Response<UpdateImageResponse>) => {
|
asyncHandler(async (req: any, res: Response<UpdateImageResponse>) => {
|
||||||
const service = getImageService();
|
const service = getImageService();
|
||||||
const { id } = req.params;
|
const { id_or_alias } = req.params;
|
||||||
|
const { flowId } = req.query;
|
||||||
const { focalPoint, meta } = req.body; // Removed alias (Section 6.1)
|
const { focalPoint, meta } = req.body; // Removed alias (Section 6.1)
|
||||||
|
|
||||||
const image = await service.getById(id);
|
// Resolve alias to imageId if needed (Section 6.2)
|
||||||
|
let imageId: string;
|
||||||
|
try {
|
||||||
|
imageId = await resolveImageIdentifier(
|
||||||
|
id_or_alias,
|
||||||
|
req.apiKey.projectId,
|
||||||
|
flowId as string | undefined
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: error instanceof Error ? error.message : 'Image not found',
|
||||||
|
code: 'IMAGE_NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await service.getById(imageId);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -577,7 +694,7 @@ imagesRouter.put(
|
||||||
if (focalPoint !== undefined) updates.focalPoint = focalPoint;
|
if (focalPoint !== undefined) updates.focalPoint = focalPoint;
|
||||||
if (meta !== undefined) updates.meta = meta;
|
if (meta !== undefined) updates.meta = meta;
|
||||||
|
|
||||||
const updated = await service.update(id, updates);
|
const updated = await service.update(imageId, updates);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -597,11 +714,13 @@ imagesRouter.put(
|
||||||
*
|
*
|
||||||
* This is a dedicated endpoint introduced in Section 6.1 to separate
|
* This is a dedicated endpoint introduced in Section 6.1 to separate
|
||||||
* alias assignment from general metadata updates.
|
* alias assignment from general metadata updates.
|
||||||
|
* Supports both UUID and alias (@-prefixed) identifiers per Section 6.2.
|
||||||
*
|
*
|
||||||
* @route PUT /api/v1/images/:id/alias
|
* @route PUT /api/v1/images/:id_or_alias/alias
|
||||||
* @authentication Project Key required
|
* @authentication Project Key required
|
||||||
*
|
*
|
||||||
* @param {string} req.params.id - Image ID (UUID)
|
* @param {string} req.params.id_or_alias - Image ID (UUID) or alias (@-prefixed)
|
||||||
|
* @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution
|
||||||
* @param {object} req.body - Request body
|
* @param {object} req.body - Request body
|
||||||
* @param {string} req.body.alias - Project-scoped alias (e.g., "@hero-bg")
|
* @param {string} req.body.alias - Project-scoped alias (e.g., "@hero-bg")
|
||||||
*
|
*
|
||||||
|
|
@ -615,19 +734,32 @@ imagesRouter.put(
|
||||||
* @throws {Error} VALIDATION_ERROR - Alias is required
|
* @throws {Error} VALIDATION_ERROR - Alias is required
|
||||||
* @throws {Error} ALIAS_CONFLICT - Alias already assigned to another image
|
* @throws {Error} ALIAS_CONFLICT - Alias already assigned to another image
|
||||||
*
|
*
|
||||||
* @example
|
* @example UUID identifier
|
||||||
* PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias
|
* PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias
|
||||||
* {
|
* {
|
||||||
* "alias": "@hero-background"
|
* "alias": "@hero-background"
|
||||||
* }
|
* }
|
||||||
|
*
|
||||||
|
* @example Project-scoped alias identifier
|
||||||
|
* PUT /api/v1/images/@old-hero/alias
|
||||||
|
* {
|
||||||
|
* "alias": "@new-hero"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example Flow-scoped alias identifier
|
||||||
|
* PUT /api/v1/images/@temp-product/alias?flowId=123e4567-e89b-12d3-a456-426614174000
|
||||||
|
* {
|
||||||
|
* "alias": "@final-product"
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
imagesRouter.put(
|
imagesRouter.put(
|
||||||
'/:id/alias',
|
'/:id_or_alias/alias',
|
||||||
validateApiKey,
|
validateApiKey,
|
||||||
requireProjectKey,
|
requireProjectKey,
|
||||||
asyncHandler(async (req: any, res: Response<UpdateImageResponse>) => {
|
asyncHandler(async (req: any, res: Response<UpdateImageResponse>) => {
|
||||||
const service = getImageService();
|
const service = getImageService();
|
||||||
const { id } = req.params;
|
const { id_or_alias } = req.params;
|
||||||
|
const { flowId } = req.query;
|
||||||
const { alias } = req.body;
|
const { alias } = req.body;
|
||||||
|
|
||||||
if (!alias || typeof alias !== 'string') {
|
if (!alias || typeof alias !== 'string') {
|
||||||
|
|
@ -641,7 +773,26 @@ imagesRouter.put(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await service.getById(id);
|
// Resolve alias to imageId if needed (Section 6.2)
|
||||||
|
let imageId: string;
|
||||||
|
try {
|
||||||
|
imageId = await resolveImageIdentifier(
|
||||||
|
id_or_alias,
|
||||||
|
req.apiKey.projectId,
|
||||||
|
flowId as string | undefined
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: error instanceof Error ? error.message : 'Image not found',
|
||||||
|
code: 'IMAGE_NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await service.getById(imageId);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -664,7 +815,7 @@ imagesRouter.put(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await service.assignProjectAlias(id, alias);
|
const updated = await service.assignProjectAlias(imageId, alias);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -685,11 +836,13 @@ imagesRouter.put(
|
||||||
*
|
*
|
||||||
* Use with caution: This is a destructive operation that permanently removes
|
* Use with caution: This is a destructive operation that permanently removes
|
||||||
* the image file and all database references.
|
* the image file and all database references.
|
||||||
|
* Supports both UUID and alias (@-prefixed) identifiers per Section 6.2.
|
||||||
*
|
*
|
||||||
* @route DELETE /api/v1/images/:id
|
* @route DELETE /api/v1/images/:id_or_alias
|
||||||
* @authentication Project Key required
|
* @authentication Project Key required
|
||||||
*
|
*
|
||||||
* @param {string} req.params.id - Image ID (UUID)
|
* @param {string} req.params.id_or_alias - Image ID (UUID) or alias (@-prefixed)
|
||||||
|
* @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution
|
||||||
*
|
*
|
||||||
* @returns {DeleteImageResponse} 200 - Deletion confirmation with image ID
|
* @returns {DeleteImageResponse} 200 - Deletion confirmation with image ID
|
||||||
* @returns {object} 404 - Image not found or access denied
|
* @returns {object} 404 - Image not found or access denied
|
||||||
|
|
@ -697,7 +850,7 @@ imagesRouter.put(
|
||||||
*
|
*
|
||||||
* @throws {Error} IMAGE_NOT_FOUND - Image does not exist
|
* @throws {Error} IMAGE_NOT_FOUND - Image does not exist
|
||||||
*
|
*
|
||||||
* @example
|
* @example UUID identifier
|
||||||
* DELETE /api/v1/images/550e8400-e29b-41d4-a716-446655440000
|
* DELETE /api/v1/images/550e8400-e29b-41d4-a716-446655440000
|
||||||
*
|
*
|
||||||
* Response:
|
* Response:
|
||||||
|
|
@ -705,16 +858,42 @@ imagesRouter.put(
|
||||||
* "success": true,
|
* "success": true,
|
||||||
* "data": { "id": "550e8400-e29b-41d4-a716-446655440000" }
|
* "data": { "id": "550e8400-e29b-41d4-a716-446655440000" }
|
||||||
* }
|
* }
|
||||||
|
*
|
||||||
|
* @example Project-scoped alias
|
||||||
|
* DELETE /api/v1/images/@old-banner
|
||||||
|
*
|
||||||
|
* @example Flow-scoped alias
|
||||||
|
* DELETE /api/v1/images/@temp-image?flowId=123e4567-e89b-12d3-a456-426614174000
|
||||||
*/
|
*/
|
||||||
imagesRouter.delete(
|
imagesRouter.delete(
|
||||||
'/:id',
|
'/:id_or_alias',
|
||||||
validateApiKey,
|
validateApiKey,
|
||||||
requireProjectKey,
|
requireProjectKey,
|
||||||
asyncHandler(async (req: any, res: Response<DeleteImageResponse>) => {
|
asyncHandler(async (req: any, res: Response<DeleteImageResponse>) => {
|
||||||
const service = getImageService();
|
const service = getImageService();
|
||||||
const { id } = req.params;
|
const { id_or_alias } = req.params;
|
||||||
|
const { flowId } = req.query;
|
||||||
|
|
||||||
const image = await service.getById(id);
|
// Resolve alias to imageId if needed (Section 6.2)
|
||||||
|
let imageId: string;
|
||||||
|
try {
|
||||||
|
imageId = await resolveImageIdentifier(
|
||||||
|
id_or_alias,
|
||||||
|
req.apiKey.projectId,
|
||||||
|
flowId as string | undefined
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: error instanceof Error ? error.message : 'Image not found',
|
||||||
|
code: 'IMAGE_NOT_FOUND',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await service.getById(imageId);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -737,11 +916,11 @@ imagesRouter.delete(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await service.hardDelete(id);
|
await service.hardDelete(imageId);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { id },
|
data: { id: imageId },
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -196,42 +196,43 @@ export class AliasService {
|
||||||
throw new Error(reservedResult.error!.message);
|
throw new Error(reservedResult.error!.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flowId) {
|
// NOTE: Conflict checks removed per Section 5.2 of api-refactoring-final.md
|
||||||
await this.checkFlowAliasConflict(alias, flowId, projectId);
|
// Aliases now use override behavior - new requests take priority over existing aliases
|
||||||
} else {
|
// Flow alias conflicts are handled by JSONB field overwrite (no check needed)
|
||||||
await this.checkProjectAliasConflict(alias, projectId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkProjectAliasConflict(alias: string, projectId: string): Promise<void> {
|
// DEPRECATED: Removed per Section 5.2 - aliases now use override behavior
|
||||||
const existing = await db.query.images.findFirst({
|
// private async checkProjectAliasConflict(alias: string, projectId: string): Promise<void> {
|
||||||
where: and(
|
// const existing = await db.query.images.findFirst({
|
||||||
eq(images.projectId, projectId),
|
// where: and(
|
||||||
eq(images.alias, alias),
|
// eq(images.projectId, projectId),
|
||||||
isNull(images.deletedAt),
|
// eq(images.alias, alias),
|
||||||
isNull(images.flowId)
|
// isNull(images.deletedAt),
|
||||||
),
|
// isNull(images.flowId)
|
||||||
});
|
// ),
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// if (existing) {
|
||||||
|
// throw new Error(ERROR_MESSAGES.ALIAS_CONFLICT);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (existing) {
|
// DEPRECATED: Removed per Section 5.2 - flow aliases now use override behavior
|
||||||
throw new Error(ERROR_MESSAGES.ALIAS_CONFLICT);
|
// Flow alias conflicts are naturally handled by JSONB field overwrite in assignFlowAlias()
|
||||||
}
|
// private async checkFlowAliasConflict(alias: string, flowId: string, projectId: string): Promise<void> {
|
||||||
}
|
// const flow = await db.query.flows.findFirst({
|
||||||
|
// where: and(eq(flows.id, flowId), eq(flows.projectId, projectId)),
|
||||||
private async checkFlowAliasConflict(alias: string, flowId: string, projectId: string): Promise<void> {
|
// });
|
||||||
const flow = await db.query.flows.findFirst({
|
//
|
||||||
where: and(eq(flows.id, flowId), eq(flows.projectId, projectId)),
|
// if (!flow) {
|
||||||
});
|
// throw new Error(ERROR_MESSAGES.FLOW_NOT_FOUND);
|
||||||
|
// }
|
||||||
if (!flow) {
|
//
|
||||||
throw new Error(ERROR_MESSAGES.FLOW_NOT_FOUND);
|
// const flowAliases = flow.aliases as Record<string, string>;
|
||||||
}
|
// if (flowAliases[alias]) {
|
||||||
|
// throw new Error(ERROR_MESSAGES.ALIAS_CONFLICT);
|
||||||
const flowAliases = flow.aliases as Record<string, string>;
|
// }
|
||||||
if (flowAliases[alias]) {
|
// }
|
||||||
throw new Error(ERROR_MESSAGES.ALIAS_CONFLICT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async resolveMultiple(
|
async resolveMultiple(
|
||||||
aliases: string[],
|
aliases: string[],
|
||||||
|
|
|
||||||
|
|
@ -180,12 +180,21 @@ export class GenerationService {
|
||||||
fileSize: genResult.size || 0,
|
fileSize: genResult.size || 0,
|
||||||
fileHash,
|
fileHash,
|
||||||
source: 'generated',
|
source: 'generated',
|
||||||
alias: params.alias || null,
|
alias: null,
|
||||||
meta: params.meta || {},
|
meta: params.meta || {},
|
||||||
width: genResult.generatedImageData?.width ?? null,
|
width: genResult.generatedImageData?.width ?? null,
|
||||||
height: genResult.generatedImageData?.height ?? null,
|
height: genResult.generatedImageData?.height ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reassign project alias if provided (override behavior per Section 5.2)
|
||||||
|
if (params.alias) {
|
||||||
|
await this.imageService.reassignProjectAlias(
|
||||||
|
params.alias,
|
||||||
|
imageRecord.id,
|
||||||
|
params.projectId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Eager flow creation if flowAlias is provided (Section 4.2)
|
// Eager flow creation if flowAlias is provided (Section 4.2)
|
||||||
if (params.flowAlias) {
|
if (params.flowAlias) {
|
||||||
// If we have pendingFlowId, create flow and link pending generations
|
// If we have pendingFlowId, create flow and link pending generations
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,46 @@ export class ImageService {
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reassign a project-scoped alias to a new image
|
||||||
|
* Clears the alias from any existing image and assigns it to the new image
|
||||||
|
* Implements override behavior per Section 5.2 of api-refactoring-final.md
|
||||||
|
*
|
||||||
|
* @param alias - The alias to reassign (e.g., "@hero")
|
||||||
|
* @param newImageId - ID of the image to receive the alias
|
||||||
|
* @param projectId - Project ID for scope validation
|
||||||
|
*/
|
||||||
|
async reassignProjectAlias(
|
||||||
|
alias: string,
|
||||||
|
newImageId: string,
|
||||||
|
projectId: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Step 1: Clear alias from any existing image with this alias
|
||||||
|
await db
|
||||||
|
.update(images)
|
||||||
|
.set({
|
||||||
|
alias: null,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(images.projectId, projectId),
|
||||||
|
eq(images.alias, alias),
|
||||||
|
isNull(images.deletedAt),
|
||||||
|
isNull(images.flowId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2: Assign alias to new image
|
||||||
|
await db
|
||||||
|
.update(images)
|
||||||
|
.set({
|
||||||
|
alias: alias,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(images.id, newImageId));
|
||||||
|
}
|
||||||
|
|
||||||
async getByStorageKey(storageKey: string): Promise<Image | null> {
|
async getByStorageKey(storageKey: string): Promise<Image | null> {
|
||||||
const image = await db.query.images.findFirst({
|
const image = await db.query.images.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,8 @@ X-API-Key: {{apiKey}}
|
||||||
###
|
###
|
||||||
|
|
||||||
### Test 7: Resolve project-scoped alias
|
### Test 7: Resolve project-scoped alias
|
||||||
# Expected: Resolves to uploadedImageId, scope=project
|
# Expected: Resolves to uploadedImageId (Section 6.2: direct alias support)
|
||||||
GET {{base}}/api/v1/images/resolve/@test-logo
|
GET {{base}}/api/v1/images/@test-logo
|
||||||
X-API-Key: {{apiKey}}
|
X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
@ -142,15 +142,15 @@ Content-Type: application/json
|
||||||
###
|
###
|
||||||
|
|
||||||
### Test 9.2: Verify new alias works
|
### Test 9.2: Verify new alias works
|
||||||
# Expected: Resolves to same uploadedImageId
|
# Expected: Resolves to same uploadedImageId (Section 6.2: direct alias support)
|
||||||
GET {{base}}/api/v1/images/resolve/@new-test-logo
|
GET {{base}}/api/v1/images/@new-test-logo
|
||||||
X-API-Key: {{apiKey}}
|
X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
### Test 10: Verify old alias doesn't work after update
|
### Test 10: Verify old alias doesn't work after update
|
||||||
# Expected: 404 - Alias not found
|
# Expected: 404 - Alias not found
|
||||||
GET {{base}}/api/v1/images/resolve/@test-logo
|
GET {{base}}/api/v1/images/@test-logo
|
||||||
X-API-Key: {{apiKey}}
|
X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
@ -176,7 +176,7 @@ X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
### Test 11.3: Verify alias resolution fails
|
### Test 11.3: Verify alias resolution fails
|
||||||
# Expected: 404 - Alias not found
|
# Expected: 404 - Alias not found
|
||||||
GET {{base}}/api/v1/images/resolve/@new-test-logo
|
GET {{base}}/api/v1/images/@new-test-logo
|
||||||
X-API-Key: {{apiKey}}
|
X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
@ -283,8 +283,8 @@ X-API-Key: {{apiKey}}
|
||||||
###
|
###
|
||||||
|
|
||||||
### Test 14.4: Verify alias resolution works
|
### Test 14.4: Verify alias resolution works
|
||||||
# Expected: Resolves to heroImageId
|
# Expected: Resolves to heroImageId (Section 6.2: direct alias support)
|
||||||
GET {{base}}/api/v1/images/resolve/@hero-banner
|
GET {{base}}/api/v1/images/@hero-banner
|
||||||
X-API-Key: {{apiKey}}
|
X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
@ -314,8 +314,8 @@ X-API-Key: {{apiKey}}
|
||||||
@secondHeroImageId = {{genConflict.response.body.$.data.outputImageId}}
|
@secondHeroImageId = {{genConflict.response.body.$.data.outputImageId}}
|
||||||
|
|
||||||
### Test 15.3: Verify second image has the alias
|
### Test 15.3: Verify second image has the alias
|
||||||
# Expected: Resolves to secondHeroImageId (not heroImageId)
|
# Expected: Resolves to secondHeroImageId (not heroImageId) (Section 6.2: direct alias support)
|
||||||
GET {{base}}/api/v1/images/resolve/@hero-banner
|
GET {{base}}/api/v1/images/@hero-banner
|
||||||
X-API-Key: {{apiKey}}
|
X-API-Key: {{apiKey}}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
|
||||||
|
|
@ -294,9 +294,10 @@ export async function resolveAlias(
|
||||||
alias: string,
|
alias: string,
|
||||||
flowId?: string
|
flowId?: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
// Section 6.2: Use direct alias identifier instead of /resolve/ endpoint
|
||||||
const endpoint = flowId
|
const endpoint = flowId
|
||||||
? `${endpoints.images}/resolve/${alias}?flowId=${flowId}`
|
? `${endpoints.images}/${alias}?flowId=${flowId}`
|
||||||
: `${endpoints.images}/resolve/${alias}`;
|
: `${endpoints.images}/${alias}`;
|
||||||
|
|
||||||
const result = await api(endpoint);
|
const result = await api(endpoint);
|
||||||
return result.data.data;
|
return result.data.data;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue