first-update
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db/index';
|
||||
import { buildEvalQuestionWhere } from '@/lib/db/evalDatasets';
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* Convert an evaluation item to a CSV row
|
||||
*/
|
||||
function convertToCSVRow(item, isHeader = false) {
|
||||
if (isHeader) {
|
||||
return ['questionType', 'question', 'options', 'correctAnswer', 'tags'].join(',');
|
||||
}
|
||||
|
||||
const escapeCSV = str => {
|
||||
if (str === null || str === undefined) return '';
|
||||
const strValue = String(str);
|
||||
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
||||
return `"${strValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return strValue;
|
||||
};
|
||||
|
||||
return [
|
||||
escapeCSV(item.questionType),
|
||||
escapeCSV(item.question),
|
||||
escapeCSV(item.options),
|
||||
escapeCSV(item.correctAnswer),
|
||||
escapeCSV(item.tags)
|
||||
].join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an evaluation item to export format
|
||||
*/
|
||||
function formatExportItem(item) {
|
||||
return {
|
||||
questionType: item.questionType,
|
||||
question: item.question,
|
||||
options: item.options,
|
||||
correctAnswer: item.correctAnswer,
|
||||
tags: item.tags
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export evaluation datasets
|
||||
* Supports JSON, JSONL, and CSV
|
||||
* Uses batched streaming for large datasets
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const body = await request.json();
|
||||
|
||||
const {
|
||||
format = 'json', // json | jsonl | csv
|
||||
questionTypes = [],
|
||||
tags = [],
|
||||
keyword = ''
|
||||
} = body;
|
||||
|
||||
// Validate format
|
||||
if (!['json', 'jsonl', 'csv'].includes(format)) {
|
||||
return NextResponse.json({ code: 400, error: 'Unsupported export format' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build query conditions
|
||||
const where = buildEvalQuestionWhere(projectId, {
|
||||
questionTypes: questionTypes.length > 0 ? questionTypes : undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
keyword: keyword || undefined
|
||||
});
|
||||
|
||||
// Fetch total count
|
||||
const total = await db.evalDatasets.count({ where });
|
||||
|
||||
if (total === 0) {
|
||||
return NextResponse.json({ code: 400, error: 'No data matches the criteria' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Return directly for small datasets
|
||||
if (total <= 1000) {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
orderBy: { createAt: 'desc' }
|
||||
});
|
||||
|
||||
const formattedItems = items.map(formatExportItem);
|
||||
|
||||
if (format === 'json') {
|
||||
return new Response(JSON.stringify(formattedItems, null, 2), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.json"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (format === 'jsonl') {
|
||||
const jsonlContent = formattedItems.map(item => JSON.stringify(item)).join('\n');
|
||||
return new Response(jsonlContent, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.jsonl"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
const csvContent = [convertToCSVRow(null, true), ...items.map(item => convertToCSVRow(item))].join('\n');
|
||||
return new Response('\uFEFF' + csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv; charset=utf-8',
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.csv"`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stream export for large datasets
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
let isFirst = true;
|
||||
|
||||
// CSV outputs header row first
|
||||
if (format === 'csv') {
|
||||
controller.enqueue(encoder.encode('\uFEFF' + convertToCSVRow(null, true) + '\n'));
|
||||
}
|
||||
|
||||
// JSON outputs opening bracket
|
||||
if (format === 'json') {
|
||||
controller.enqueue(encoder.encode('[\n'));
|
||||
}
|
||||
|
||||
// Fetch data in batches
|
||||
const totalBatches = Math.ceil(total / BATCH_SIZE);
|
||||
|
||||
for (let batch = 0; batch < totalBatches; batch++) {
|
||||
const items = await db.evalDatasets.findMany({
|
||||
where,
|
||||
orderBy: { createAt: 'desc' },
|
||||
skip: batch * BATCH_SIZE,
|
||||
take: BATCH_SIZE
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
const formattedItem = formatExportItem(item);
|
||||
|
||||
if (format === 'json') {
|
||||
const prefix = isFirst ? '' : ',\n';
|
||||
controller.enqueue(encoder.encode(prefix + JSON.stringify(formattedItem)));
|
||||
isFirst = false;
|
||||
} else if (format === 'jsonl') {
|
||||
controller.enqueue(encoder.encode(JSON.stringify(formattedItem) + '\n'));
|
||||
} else if (format === 'csv') {
|
||||
controller.enqueue(encoder.encode(convertToCSVRow(item) + '\n'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON outputs closing bracket
|
||||
if (format === 'json') {
|
||||
controller.enqueue(encoder.encode('\n]'));
|
||||
}
|
||||
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const contentTypes = {
|
||||
json: 'application/json',
|
||||
jsonl: 'application/x-ndjson',
|
||||
csv: 'text/csv; charset=utf-8'
|
||||
};
|
||||
|
||||
const extensions = {
|
||||
json: 'json',
|
||||
jsonl: 'jsonl',
|
||||
csv: 'csv'
|
||||
};
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': contentTypes[format],
|
||||
'Content-Disposition': `attachment; filename="eval-datasets-${Date.now()}.${extensions[format]}"`,
|
||||
'Transfer-Encoding': 'chunked'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to export eval datasets:', error);
|
||||
return NextResponse.json({ code: 500, error: error.message || 'Export failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get export preview (count only)
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Parse query params
|
||||
const questionTypes = searchParams.getAll('questionTypes');
|
||||
const tags = searchParams.getAll('tags');
|
||||
const keyword = searchParams.get('keyword') || '';
|
||||
|
||||
// Build query conditions
|
||||
const where = buildEvalQuestionWhere(projectId, {
|
||||
questionTypes: questionTypes.length > 0 ? questionTypes : undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
keyword: keyword || undefined
|
||||
});
|
||||
|
||||
// Count rows
|
||||
const total = await db.evalDatasets.count({ where });
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
data: {
|
||||
total,
|
||||
isLargeDataset: total > 1000
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get export preview:', error);
|
||||
return NextResponse.json({ code: 500, error: error.message || 'Failed to get export preview' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user