345 lines
9.3 KiB
TypeScript
345 lines
9.3 KiB
TypeScript
import { Response, Router } from 'express';
|
|
import type { Router as RouterType } from 'express';
|
|
import { LiveScopeService, ImageService, GenerationService } from '@/services/core';
|
|
import { asyncHandler } from '@/middleware/errorHandler';
|
|
import { validateApiKey } from '@/middleware/auth/validateApiKey';
|
|
import { requireProjectKey } from '@/middleware/auth/requireProjectKey';
|
|
import { rateLimitByApiKey } from '@/middleware/auth/rateLimiter';
|
|
import { PAGINATION_LIMITS, ERROR_MESSAGES } from '@/utils/constants';
|
|
import { buildPaginationMeta } from '@/utils/helpers';
|
|
import { toLiveScopeResponse, toImageResponse } from '@/types/responses';
|
|
import type {
|
|
CreateLiveScopeRequest,
|
|
ListLiveScopesQuery,
|
|
UpdateLiveScopeRequest,
|
|
RegenerateScopeRequest,
|
|
} from '@/types/requests';
|
|
import type {
|
|
CreateLiveScopeResponse,
|
|
GetLiveScopeResponse,
|
|
ListLiveScopesResponse,
|
|
UpdateLiveScopeResponse,
|
|
DeleteLiveScopeResponse,
|
|
RegenerateScopeResponse,
|
|
} from '@/types/responses';
|
|
|
|
export const scopesRouter: RouterType = Router();
|
|
|
|
let scopeService: LiveScopeService;
|
|
let imageService: ImageService;
|
|
let generationService: GenerationService;
|
|
|
|
const getScopeService = (): LiveScopeService => {
|
|
if (!scopeService) {
|
|
scopeService = new LiveScopeService();
|
|
}
|
|
return scopeService;
|
|
};
|
|
|
|
const getImageService = (): ImageService => {
|
|
if (!imageService) {
|
|
imageService = new ImageService();
|
|
}
|
|
return imageService;
|
|
};
|
|
|
|
const getGenerationService = (): GenerationService => {
|
|
if (!generationService) {
|
|
generationService = new GenerationService();
|
|
}
|
|
return generationService;
|
|
};
|
|
|
|
/**
|
|
* POST /api/v1/live/scopes
|
|
* Create new live scope manually (Section 8.5)
|
|
* @authentication Project Key required
|
|
*/
|
|
scopesRouter.post(
|
|
'/',
|
|
validateApiKey,
|
|
requireProjectKey,
|
|
rateLimitByApiKey,
|
|
asyncHandler(async (req: any, res: Response<CreateLiveScopeResponse>) => {
|
|
const service = getScopeService();
|
|
const { slug, allowNewGenerations, newGenerationsLimit, meta } = req.body as CreateLiveScopeRequest;
|
|
const projectId = req.apiKey.projectId;
|
|
|
|
// Validate slug format
|
|
if (!slug || !/^[a-zA-Z0-9_-]+$/.test(slug)) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: ERROR_MESSAGES.SCOPE_INVALID_FORMAT,
|
|
code: 'SCOPE_INVALID_FORMAT',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Check if scope already exists
|
|
const existing = await service.getBySlug(projectId, slug);
|
|
if (existing) {
|
|
res.status(409).json({
|
|
success: false,
|
|
error: {
|
|
message: 'Scope with this slug already exists',
|
|
code: 'SCOPE_ALREADY_EXISTS',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Create scope
|
|
const scope = await service.create({
|
|
projectId,
|
|
slug,
|
|
allowNewGenerations: allowNewGenerations ?? true,
|
|
newGenerationsLimit: newGenerationsLimit ?? 30,
|
|
meta: meta || {},
|
|
});
|
|
|
|
// Get with stats
|
|
const scopeWithStats = await service.getByIdWithStats(scope.id);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: toLiveScopeResponse(scopeWithStats),
|
|
});
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* GET /api/v1/live/scopes
|
|
* List all live scopes for a project (Section 8.5)
|
|
* @authentication Project Key required
|
|
*/
|
|
scopesRouter.get(
|
|
'/',
|
|
validateApiKey,
|
|
requireProjectKey,
|
|
asyncHandler(async (req: any, res: Response<ListLiveScopesResponse>) => {
|
|
const service = getScopeService();
|
|
const { slug, limit, offset } = req.query as ListLiveScopesQuery;
|
|
const projectId = req.apiKey.projectId;
|
|
|
|
const parsedLimit = Math.min(
|
|
(limit ? parseInt(limit.toString(), 10) : PAGINATION_LIMITS.DEFAULT_LIMIT) || PAGINATION_LIMITS.DEFAULT_LIMIT,
|
|
PAGINATION_LIMITS.MAX_LIMIT,
|
|
);
|
|
const parsedOffset = (offset ? parseInt(offset.toString(), 10) : 0) || 0;
|
|
|
|
const result = await service.list(
|
|
{ projectId, slug },
|
|
parsedLimit,
|
|
parsedOffset,
|
|
);
|
|
|
|
const scopeResponses = result.scopes.map(toLiveScopeResponse);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: scopeResponses,
|
|
pagination: buildPaginationMeta(result.total, parsedLimit, parsedOffset),
|
|
});
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* GET /api/v1/live/scopes/:slug
|
|
* Get single live scope by slug (Section 8.5)
|
|
* @authentication Project Key required
|
|
*/
|
|
scopesRouter.get(
|
|
'/:slug',
|
|
validateApiKey,
|
|
requireProjectKey,
|
|
asyncHandler(async (req: any, res: Response<GetLiveScopeResponse>) => {
|
|
const service = getScopeService();
|
|
const { slug } = req.params;
|
|
const projectId = req.apiKey.projectId;
|
|
|
|
const scopeWithStats = await service.getBySlugWithStats(projectId, slug);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: toLiveScopeResponse(scopeWithStats),
|
|
});
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* PUT /api/v1/live/scopes/:slug
|
|
* Update live scope settings (Section 8.5)
|
|
* @authentication Project Key required
|
|
*/
|
|
scopesRouter.put(
|
|
'/:slug',
|
|
validateApiKey,
|
|
requireProjectKey,
|
|
rateLimitByApiKey,
|
|
asyncHandler(async (req: any, res: Response<UpdateLiveScopeResponse>) => {
|
|
const service = getScopeService();
|
|
const { slug } = req.params;
|
|
const { allowNewGenerations, newGenerationsLimit, meta } = req.body as UpdateLiveScopeRequest;
|
|
const projectId = req.apiKey.projectId;
|
|
|
|
// Get scope
|
|
const scope = await service.getBySlugOrThrow(projectId, slug);
|
|
|
|
// Update scope
|
|
const updates: {
|
|
allowNewGenerations?: boolean;
|
|
newGenerationsLimit?: number;
|
|
meta?: Record<string, unknown>;
|
|
} = {};
|
|
if (allowNewGenerations !== undefined) updates.allowNewGenerations = allowNewGenerations;
|
|
if (newGenerationsLimit !== undefined) updates.newGenerationsLimit = newGenerationsLimit;
|
|
if (meta !== undefined) updates.meta = meta;
|
|
|
|
await service.update(scope.id, updates);
|
|
|
|
// Get updated scope with stats
|
|
const updated = await service.getByIdWithStats(scope.id);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: toLiveScopeResponse(updated),
|
|
});
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* POST /api/v1/live/scopes/:slug/regenerate
|
|
* Regenerate images in scope (Section 8.5)
|
|
* @authentication Project Key required
|
|
*/
|
|
scopesRouter.post(
|
|
'/:slug/regenerate',
|
|
validateApiKey,
|
|
requireProjectKey,
|
|
rateLimitByApiKey,
|
|
asyncHandler(async (req: any, res: Response<RegenerateScopeResponse>) => {
|
|
const scopeService = getScopeService();
|
|
const imgService = getImageService();
|
|
const genService = getGenerationService();
|
|
const { slug } = req.params;
|
|
const { imageId } = req.body as RegenerateScopeRequest;
|
|
const projectId = req.apiKey.projectId;
|
|
|
|
// Get scope
|
|
const scope = await scopeService.getBySlugWithStats(projectId, slug);
|
|
|
|
if (imageId) {
|
|
// Regenerate specific image
|
|
const image = await imgService.getById(imageId);
|
|
if (!image) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: {
|
|
message: ERROR_MESSAGES.IMAGE_NOT_FOUND,
|
|
code: 'IMAGE_NOT_FOUND',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Check if image belongs to this scope
|
|
const imageMeta = image.meta as Record<string, unknown>;
|
|
if (imageMeta['scope'] !== slug) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'Image does not belong to this scope',
|
|
code: 'IMAGE_NOT_IN_SCOPE',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Regenerate the image's generation
|
|
if (image.generationId) {
|
|
await genService.regenerate(image.generationId);
|
|
}
|
|
|
|
const regeneratedImage = await imgService.getById(imageId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
regenerated: 1,
|
|
images: regeneratedImage ? [toImageResponse(regeneratedImage)] : [],
|
|
},
|
|
});
|
|
} else {
|
|
// Regenerate all images in scope
|
|
if (!scope.images || scope.images.length === 0) {
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
regenerated: 0,
|
|
images: [],
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const regeneratedImages = [];
|
|
for (const image of scope.images) {
|
|
if (image.generationId) {
|
|
await genService.regenerate(image.generationId);
|
|
const regenerated = await imgService.getById(image.id);
|
|
if (regenerated) {
|
|
regeneratedImages.push(toImageResponse(regenerated));
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
regenerated: regeneratedImages.length,
|
|
images: regeneratedImages,
|
|
},
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* DELETE /api/v1/live/scopes/:slug
|
|
* Delete live scope (Section 8.5)
|
|
* Deletes all images in scope following standard deletion rules
|
|
* @authentication Project Key required
|
|
*/
|
|
scopesRouter.delete(
|
|
'/:slug',
|
|
validateApiKey,
|
|
requireProjectKey,
|
|
rateLimitByApiKey,
|
|
asyncHandler(async (req: any, res: Response<DeleteLiveScopeResponse>) => {
|
|
const scopeService = getScopeService();
|
|
const imgService = getImageService();
|
|
const { slug } = req.params;
|
|
const projectId = req.apiKey.projectId;
|
|
|
|
// Get scope with images
|
|
const scope = await scopeService.getBySlugWithStats(projectId, slug);
|
|
|
|
// Delete all images in scope (follows alias protection rules)
|
|
if (scope.images) {
|
|
for (const image of scope.images) {
|
|
await imgService.hardDelete(image.id);
|
|
}
|
|
}
|
|
|
|
// Delete scope record
|
|
await scopeService.delete(scope.id);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { id: scope.id },
|
|
});
|
|
}),
|
|
);
|