fix: aliases

This commit is contained in:
Oleg Proskurin 2025-11-25 22:47:54 +07:00
parent 88cb1f2c61
commit 8623442157
6 changed files with 305 additions and 75 deletions

View File

@ -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 },
}); });
}) })
); );

View File

@ -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[],

View File

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

View File

@ -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(

View File

@ -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}}
### ###

View File

@ -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;