feat: implement backend API endpoints
- CSV upload with papaparse (handles date strings and Unix timestamps) - Annotations CRUD (GET, POST, DELETE) - Candles GET endpoint - Export annotations as CSV
This commit is contained in:
parent
d04b673cfa
commit
096a80b229
5 changed files with 312 additions and 0 deletions
134
src/app/api/upload/route.ts
Normal file
134
src/app/api/upload/route.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import Papa from 'papaparse';
|
||||
import { db } from '@/lib/db';
|
||||
import { candles } from '@/lib/db/schema';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No file provided' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const text = await file.text();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Papa.parse(text, {
|
||||
header: true,
|
||||
dynamicTyping: true,
|
||||
skipEmptyLines: true,
|
||||
complete: async (results) => {
|
||||
try {
|
||||
const rows = results.data as any[];
|
||||
|
||||
// Validate headers
|
||||
if (rows.length === 0) {
|
||||
resolve(
|
||||
NextResponse.json(
|
||||
{ error: 'CSV file is empty' },
|
||||
{ status: 400 }
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstRow = rows[0];
|
||||
const requiredHeaders = ['time', 'open', 'high', 'low', 'close'];
|
||||
const missingHeaders = requiredHeaders.filter(
|
||||
(header) => !(header in firstRow)
|
||||
);
|
||||
|
||||
if (missingHeaders.length > 0) {
|
||||
resolve(
|
||||
NextResponse.json(
|
||||
{
|
||||
error: `Missing required headers: ${missingHeaders.join(', ')}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and prepare candle data
|
||||
const candleData = rows.map((row) => {
|
||||
let timestamp: number;
|
||||
|
||||
// Handle both date strings and Unix timestamps
|
||||
if (typeof row.time === 'string') {
|
||||
// Try parsing as date string
|
||||
const date = new Date(row.time);
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error(`Invalid date format: ${row.time}`);
|
||||
}
|
||||
timestamp = Math.floor(date.getTime() / 1000);
|
||||
} else if (typeof row.time === 'number') {
|
||||
timestamp = row.time;
|
||||
} else {
|
||||
throw new Error(`Invalid time value: ${row.time}`);
|
||||
}
|
||||
|
||||
return {
|
||||
time: timestamp,
|
||||
open: Number(row.open),
|
||||
high: Number(row.high),
|
||||
low: Number(row.low),
|
||||
close: Number(row.close),
|
||||
};
|
||||
});
|
||||
|
||||
// Insert with upsert behavior (replace on conflict)
|
||||
let count = 0;
|
||||
for (const candle of candleData) {
|
||||
await db
|
||||
.insert(candles)
|
||||
.values(candle)
|
||||
.onConflictDoUpdate({
|
||||
target: candles.time,
|
||||
set: {
|
||||
open: candle.open,
|
||||
high: candle.high,
|
||||
low: candle.low,
|
||||
close: candle.close,
|
||||
},
|
||||
});
|
||||
count++;
|
||||
}
|
||||
|
||||
resolve(
|
||||
NextResponse.json({
|
||||
success: true,
|
||||
count,
|
||||
})
|
||||
);
|
||||
} catch (error: any) {
|
||||
resolve(
|
||||
NextResponse.json(
|
||||
{ error: error.message || 'Failed to process CSV data' },
|
||||
{ status: 400 }
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
resolve(
|
||||
NextResponse.json(
|
||||
{ error: `CSV parsing error: ${error.message}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue