w-funnel/src/app/api/funnels/[id]/route.ts
dev.daminik00 0ceb254f4e hh
2025-09-29 06:10:56 +02:00

267 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server';
import { adminApiDisabledResponse, isAdminApiEnabled } from '@/lib/runtime/adminApi';
import type { FunnelDefinition } from '@/lib/funnel/types';
interface RouteParams {
params: Promise<{
id: string;
}>;
}
// No normalization needed: we require `progressbars` for loaders
// GET /api/funnels/[id] - получить конкретную воронку
export async function GET(request: NextRequest, { params }: RouteParams) {
if (!isAdminApiEnabled()) {
return adminApiDisabledResponse();
}
try {
const { id } = await params;
const [{ default: connectMongoDB }, { default: FunnelModel }] = await Promise.all([
import('@/lib/mongodb'),
import('@/lib/models/Funnel'),
]);
await connectMongoDB();
const funnel = await FunnelModel.findById(id);
if (!funnel) {
return NextResponse.json(
{ error: 'Funnel not found' },
{ status: 404 }
);
}
return NextResponse.json({
_id: funnel._id,
name: funnel.name,
description: funnel.description,
status: funnel.status,
version: funnel.version,
createdAt: funnel.createdAt,
updatedAt: funnel.updatedAt,
publishedAt: funnel.publishedAt,
usage: funnel.usage,
funnelData: funnel.funnelData
});
} catch (error) {
console.error('GET /api/funnels/[id] error:', error);
return NextResponse.json(
{ error: 'Failed to fetch funnel' },
{ status: 500 }
);
}
}
// PUT /api/funnels/[id] - обновить воронку
export async function PUT(request: NextRequest, { params }: RouteParams) {
if (!isAdminApiEnabled()) {
return adminApiDisabledResponse();
}
try {
const { id } = await params;
const [
{ default: connectMongoDB },
{ default: FunnelModel },
{ default: FunnelHistoryModel },
] = await Promise.all([
import('@/lib/mongodb'),
import('@/lib/models/Funnel'),
import('@/lib/models/FunnelHistory'),
]);
await connectMongoDB();
const body = await request.json();
const { name, description, funnelData, status, sessionId, actionDescription } = body;
// Валидация
if (funnelData && (!funnelData.meta || !funnelData.meta.id || !Array.isArray(funnelData.screens))) {
return NextResponse.json(
{ error: 'Invalid funnel data structure' },
{ status: 400 }
);
}
const funnel = await FunnelModel.findById(id);
if (!funnel) {
return NextResponse.json(
{ error: 'Funnel not found' },
{ status: 404 }
);
}
// Сохраняем предыдущее состояние для истории
const previousData = funnel.funnelData;
// Обновляем поля
if (name !== undefined) funnel.name = name;
if (description !== undefined) funnel.description = description;
// Логика версионирования:
// - При сохранении (без смены статуса) - версия НЕ увеличивается
// - При публикации - версия увеличивается и меняется статус
const isPublishing = status === 'published' && funnel.status !== 'published';
if (status !== undefined) funnel.status = status;
if (funnelData !== undefined) {
// Save as-is; schema expects `progressbars` for loaders
funnel.funnelData = funnelData as FunnelDefinition;
// Увеличиваем версию только при публикации
if (isPublishing) {
funnel.version += 1;
funnel.publishedAt = new Date();
}
}
funnel.lastModifiedBy = 'current-user'; // TODO: заменить на реального пользователя
const savedFunnel = await funnel.save();
// Создаем запись в истории, если обновлялась структура воронки
if (funnelData && sessionId) {
// Получаем текущий номер последовательности
const lastHistoryEntry = await FunnelHistoryModel
.findOne({ funnelId: id, sessionId })
.sort({ sequenceNumber: -1 });
const nextSequenceNumber = (lastHistoryEntry?.sequenceNumber || -1) + 1;
await FunnelHistoryModel.create({
funnelId: id,
sessionId,
funnelSnapshot: funnelData as FunnelDefinition,
actionType: status === 'published' ? 'publish' : 'update',
sequenceNumber: nextSequenceNumber,
description: actionDescription || 'Воронка обновлена',
isBaseline: status === 'published',
changeDetails: {
action: 'update-funnel',
previousValue: previousData,
newValue: funnelData as FunnelDefinition
}
});
// Очищаем старые записи истории (оставляем 100)
const KEEP_ENTRIES = 100;
const entriesToDelete = await FunnelHistoryModel
.find({ funnelId: id, sessionId })
.sort({ sequenceNumber: -1 })
.skip(KEEP_ENTRIES)
.select('_id');
if (entriesToDelete.length > 0) {
const idsToDelete = entriesToDelete.map((entry: { _id: unknown }) => entry._id);
await FunnelHistoryModel.deleteMany({ _id: { $in: idsToDelete } });
}
}
return NextResponse.json({
_id: savedFunnel._id,
name: savedFunnel.name,
description: savedFunnel.description,
status: savedFunnel.status,
version: savedFunnel.version,
createdAt: savedFunnel.createdAt,
updatedAt: savedFunnel.updatedAt,
publishedAt: savedFunnel.publishedAt,
usage: savedFunnel.usage,
funnelData: savedFunnel.funnelData
});
} catch (error) {
console.error('PUT /api/funnels/[id] error:', error);
// Обработка ошибок валидации Mongoose
if (error instanceof Error && error.name === 'ValidationError') {
const validationError = error as Error & { errors: Record<string, { message: string }> };
const details = [];
// Собираем все ошибки валидации
for (const field in validationError.errors) {
const fieldError = validationError.errors[field];
details.push(`${field}: ${fieldError.message}`);
}
return NextResponse.json(
{
error: 'Ошибка валидации данных воронки',
details: details.join('; ')
},
{ status: 400 }
);
}
// Обработка других ошибок
const errorMessage = error instanceof Error ? error.message : 'Неизвестная ошибка';
return NextResponse.json(
{
error: 'Ошибка сохранения воронки',
details: errorMessage
},
{ status: 500 }
);
}
}
// DELETE /api/funnels/[id] - удалить воронку
export async function DELETE(request: NextRequest, { params }: RouteParams) {
if (!isAdminApiEnabled()) {
return adminApiDisabledResponse();
}
try {
const { id } = await params;
const [
{ default: connectMongoDB },
{ default: FunnelModel },
{ default: FunnelHistoryModel },
] = await Promise.all([
import('@/lib/mongodb'),
import('@/lib/models/Funnel'),
import('@/lib/models/FunnelHistory'),
]);
await connectMongoDB();
const funnel = await FunnelModel.findById(id);
if (!funnel) {
return NextResponse.json(
{ error: 'Funnel not found' },
{ status: 404 }
);
}
// Проверяем статус - опубликованные воронки нельзя удалять напрямую
if (funnel.status === 'published') {
return NextResponse.json(
{ error: 'Cannot delete published funnel. Archive it first.' },
{ status: 400 }
);
}
// Удаляем воронку и всю связанную историю
await Promise.all([
FunnelModel.findByIdAndDelete(id),
FunnelHistoryModel.deleteMany({ funnelId: id })
]);
return NextResponse.json({
message: 'Funnel deleted successfully'
});
} catch (error) {
console.error('DELETE /api/funnels/[id] error:', error);
return NextResponse.json(
{ error: 'Failed to delete funnel' },
{ status: 500 }
);
}
}