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:
Marko Djordjevic 2026-02-12 10:24:03 +01:00
parent d04b673cfa
commit 096a80b229
5 changed files with 312 additions and 0 deletions

134
src/app/api/upload/route.ts Normal file
View 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 }
);
}
}